diff --git a/loop/plan.md b/loop/plan.md
index dfad8a7c..bb039237 100644
--- a/loop/plan.md
+++ b/loop/plan.md
@@ -364,7 +364,7 @@
#### Sprint 30: Security Penetration Testing (Week 1-4)
-- [ ] **PENTEST-01** — Run automated penetration test suite. Execute `scripts/verify-pentest-fixes.sh` and `scripts/test-security.sh`. Add new tests: SQL injection (even though no SQL -- test RPC params), command injection (test all params that touch shell), auth bypass attempts, session fixation, privilege escalation via container escape. **Acceptance**: All pen tests pass.
+- [x] **PENTEST-01** — Run automated penetration test suite. Execute `scripts/verify-pentest-fixes.sh` and `scripts/test-security.sh`. Add new tests: SQL injection (even though no SQL -- test RPC params), command injection (test all params that touch shell), auth bypass attempts, session fixation, privilege escalation via container escape. **Acceptance**: All pen tests pass.
- [ ] **PENTEST-02** — Conduct manual security review of all RPC endpoints. Review each of the 80+ RPC endpoints in `core/archipelago/src/api/rpc/mod.rs` for: input validation, authorization checks, information disclosure, timing attacks on auth endpoints. Document findings. **Acceptance**: All endpoints reviewed; critical issues fixed.
diff --git a/scripts/test-security.sh b/scripts/test-security.sh
index 659701be..648ba558 100755
--- a/scripts/test-security.sh
+++ b/scripts/test-security.sh
@@ -1,12 +1,15 @@
#!/bin/bash
-set -euo pipefail
+set -uo pipefail
# SEC-201: Security penetration test covering key attack vectors.
-# Covers: auth bypass, session management, input validation, path traversal, SSRF.
+# Covers: auth bypass, session management, input validation, path traversal,
+# SSRF, command injection, session fixation, container escape.
+# Runs all tests directly against the backend HTTP API (no SSH needed for curl).
+HOST="${1:-192.168.1.228}"
+PASSWORD="${2:-password123}"
+BACKEND="http://$HOST:5678"
SSH_KEY="${ARCHIPELAGO_SSH_KEY:-$HOME/.ssh/archipelago-deploy}"
-TARGET="archipelago@192.168.1.228"
-SSH_CMD="ssh -i $SSH_KEY -o StrictHostKeyChecking=no $TARGET"
-PASSWORD="password123"
+SSH_CMD="ssh -i $SSH_KEY -o StrictHostKeyChecking=no -o ConnectTimeout=10 archipelago@$HOST"
PASS=0
FAIL=0
@@ -16,21 +19,33 @@ log() { echo -e "\033[1;34m[SEC]\033[0m $*"; }
pass() { echo -e "\033[1;32m[PASS]\033[0m $*"; PASS=$((PASS + 1)); RESULTS+=("PASS: $*"); }
fail() { echo -e "\033[1;31m[FAIL]\033[0m $*"; FAIL=$((FAIL + 1)); RESULTS+=("FAIL: $*"); }
-rpc_raw() {
- local cookie="${1:-}" method="$2" params="${3:-{}}"
- local cookie_header=""
- [ -n "$cookie" ] && cookie_header="-H 'Cookie: session=$cookie'"
- $SSH_CMD "curl -s http://localhost:5678/rpc/v1 \
+SESSION=""
+CSRF=""
+
+# Login and extract session + CSRF token
+get_auth() {
+ local login_out
+ login_out=$(curl -sv "$BACKEND/rpc/v1" \
-X POST -H 'Content-Type: application/json' \
- $cookie_header \
- -d '{\"method\":\"$method\",\"params\":$params}' 2>/dev/null"
+ -d "{\"method\":\"auth.login\",\"params\":{\"password\":\"$PASSWORD\"}}" 2>&1 || true)
+ SESSION=$(echo "$login_out" | grep -i "set-cookie.*session=" | sed 's/.*session=//;s/;.*//' | head -1)
+ CSRF=$(echo "$login_out" | grep -i "set-cookie.*csrf_token=" | sed 's/.*csrf_token=//;s/;.*//' | head -1)
}
-get_session() {
- $SSH_CMD "curl -s -c - http://localhost:5678/rpc/v1 \
- -X POST -H 'Content-Type: application/json' \
- -d '{\"method\":\"auth.login\",\"params\":{\"password\":\"$PASSWORD\"}}' 2>/dev/null \
- | grep session | awk '{print \$NF}'"
+rpc_raw() {
+ local method="$1" params="${2:-{}}"
+ curl -s --max-time 10 -X POST "$BACKEND/rpc/v1" \
+ -H 'Content-Type: application/json' \
+ -d "{\"method\":\"$method\",\"params\":$params}" 2>/dev/null || echo ""
+}
+
+rpc_auth() {
+ local method="$1" params="${2:-{}}"
+ curl -s --max-time 10 -X POST "$BACKEND/rpc/v1" \
+ -H 'Content-Type: application/json' \
+ -H "Cookie: session=$SESSION; csrf_token=$CSRF" \
+ -H "X-CSRF-Token: $CSRF" \
+ -d "{\"method\":\"$method\",\"params\":$params}" 2>/dev/null || echo ""
}
main() {
@@ -40,8 +55,8 @@ main() {
# 1. Authentication bypass — unauthenticated access to protected endpoints
log "1. Auth bypass — calling protected RPC without session..."
local result
- result=$(rpc_raw "" "container-list")
- if echo "$result" | grep -q '"code":401\|Unauthorized'; then
+ result=$(rpc_raw "container-list")
+ if echo "$result" | grep -qi '"code":401\|unauthorized'; then
pass "Protected endpoints reject unauthenticated requests"
else
fail "container-list accessible without authentication"
@@ -49,8 +64,9 @@ main() {
# 2. Auth bypass — invalid session token
log "2. Auth bypass — invalid session token..."
- result=$(rpc_raw "fake-session-token-12345" "container-list")
- if echo "$result" | grep -q '"code":401\|Unauthorized'; then
+ SESSION="fake-session-token-12345" CSRF="fake-csrf"
+ result=$(rpc_auth "container-list")
+ if echo "$result" | grep -qi '"code":401\|unauthorized\|"code":403'; then
pass "Invalid session tokens are rejected"
else
fail "Invalid session token accepted"
@@ -58,18 +74,155 @@ main() {
# 3. Auth bypass — wrong password
log "3. Auth bypass — wrong password..."
- result=$(rpc_raw "" "auth.login" '{"password":"wrongpassword"}')
+ result=$(curl -s --max-time 10 -X POST "$BACKEND/rpc/v1" \
+ -H 'Content-Type: application/json' \
+ -d '{"method":"auth.login","params":{"password":"wrongpassword"}}' 2>/dev/null || echo "")
if echo "$result" | grep -q '"error"'; then
pass "Wrong password correctly rejected"
else
fail "Wrong password accepted"
fi
- # 4. Rate limiting — multiple failed logins
- log "4. Rate limiting — rapid failed logins..."
+ # Get valid session for further tests
+ log "Getting valid session..."
+ get_auth
+ if [ ${#SESSION} -lt 10 ]; then
+ log "WARNING: Could not get valid session (len=${#SESSION})"
+ fi
+ echo ""
+
+ # 5. Input validation — SQL injection attempt in RPC params
+ log "5. Input validation — SQL injection in params..."
+ result=$(rpc_auth "identity.get" "{\"id\":\"1; DROP TABLE identities; --\"}")
+ if echo "$result" | grep -qi "drop table\|sql\|syntax error"; then
+ fail "Possible SQL injection vulnerability"
+ else
+ pass "SQL injection attempt handled safely"
+ fi
+
+ # 6. Input validation — XSS in params
+ log "6. Input validation — XSS in params..."
+ result=$(rpc_auth "identity.create" "{\"name\":\"\",\"purpose\":\"personal\"}")
+ if echo "$result" | grep -q '","purpose":"personal"}')
- if echo "$result" | grep -q '