Skip to content

Live Container Penetration Testing

This guide documents how to run an adversarial live-container penetration test against a NENE2 application — starting from setup through all 30 attack phases — and records the canonical results from the v1.5.329 test session (2026-05-31, 150+ cases).

The test adopts a cracker mindset: assume the attacker has full access to the source code (white-box), has read all public documentation, and will try every known attack class before giving up.


Prerequisites

  • Docker Compose available (docker compose version)
  • curl, nc (netcat), openssl, python3 installed on the host
  • A running NENE2 container with test credentials

1. Container Setup

Start an isolated test target. Use a dedicated port (never the production port) and inject test credentials:

bash
# PHP built-in server target — fastest to spin up, tests raw NENE2 behaviour
NENE2_MACHINE_API_KEY=pentest-key docker compose run -d --rm \
  -e NENE2_LOCAL_JWT_SECRET=pentest-jwt-secret-32chars-min!! \
  -e APP_ENV=local \
  -e APP_DEBUG=false \
  -p 8299:80 \
  app php -S 0.0.0.0:80 -t public_html/

# Apache target — tests full stack including Apache config hardening
NENE2_MACHINE_API_KEY=pentest-key docker compose up -d app
# Available on :8200 (see port registry in CLAUDE.md §8)

Baseline smoke check:

bash
curl -si http://localhost:8299/
# Expected: 200 OK, security headers present, no Server/X-Powered-By

Enumerate attack surface from OpenAPI:

bash
curl -s http://localhost:8299/openapi.php | grep -E "^  /"
# → /, /health, /machine/health, /examples/protected,
#   /examples/notes, /examples/notes/{id}, /examples/tags, /examples/tags/{id}

Generate test credentials inside the container:

bash
CID=$(docker ps --filter "publish=8299" --format "{{.ID}}")
VALID_JWT=$(docker exec $CID php -r "
  require 'vendor/autoload.php';
  \$v = new Nene2\Auth\LocalBearerTokenVerifier('pentest-jwt-secret-32chars-min!!');
  echo \$v->issue(['sub'=>'tester','exp'=>time()+86400]);
")

2. Attack Phases

Phase 1 — JWT Algorithm Confusion

IDAttackExpectedv1.5.329
J-01alg:none (empty signature)401✅ BLOCKED
J-02alg:NONE (uppercase)401✅ BLOCKED
J-03alg:None (mixed case)401✅ BLOCKED
J-04alg:hs256 (lowercase)401✅ BLOCKED
J-05alg:RS256 (key confusion)401✅ BLOCKED
J-06No alg field401✅ BLOCKED
J-07kid: ../../etc/passwd200 (valid sig)✅ SAFE — extra header fields ignored
J-08jku: http://evil.com200 (valid sig)✅ SAFE — no JWK fetch
bash
# J-01: alg:none
H=$(echo -n '{"typ":"JWT","alg":"none"}' | base64 -w0 | tr '+/' '-_' | tr -d '=')
P=$(echo -n '{"sub":"admin","exp":9999999999}' | base64 -w0 | tr '+/' '-_' | tr -d '=')
curl -si -H "Authorization: Bearer $H.$P." http://localhost:8299/examples/protected
# → 401  detail: "Token algorithm must be HS256."

Phase 1b — JWT Payload Manipulation

IDAttackExpectedv1.5.329
J-09exp: 0 (epoch 1970)401 expired✅ BLOCKED
J-10exp: null401 must be numeric✅ BLOCKED
J-11exp: "never"401 must be numeric✅ BLOCKED
J-12exp: 9999999999.9 (float)401 must be numeric✅ BLOCKED
J-13Payload is JSON array401 must be numeric✅ BLOCKED
J-14Double space in Bearer value401✅ BLOCKED
J-15No Bearer scheme401✅ BLOCKED
J-164-segment token (extra dot)401 format invalid✅ BLOCKED
J-17Header + payload only (no sig)401✅ BLOCKED

Key invariant: exp must be a present integer — absence or wrong type is rejected (fixed in v1.5.329).

Phase 2 — SQL Injection

All repositories use ? placeholder parameterized queries. No raw string interpolation.

IDAttackExpectedv1.5.329
S-01Classic ' OR 1=1-- in title201 (stored as literal)✅ SAFE
S-02UNION SELECT 1,2,3--201 (stored as literal)✅ SAFE
S-03Boolean blind AND 1=1--201 (stored as literal)✅ SAFE
S-04Time-based AND SLEEP(2)--201 in <50ms✅ SAFE — SLEEP not executed
S-05Path param SQLi /notes/1' OR '1'='1200 (int cast → 1)✅ SAFE
S-06Null byte \0' OR '1'='1201 (literal)✅ SAFE
S-07Second-order: store payload, then read200 (literal re-read)✅ SAFE
S-08SLEEP(5) in body field201 in <50ms✅ SAFE
S-10limit=UNION SELECT... in query string422 (validation)✅ SAFE
bash
# Verify parameterized queries: SLEEP is not executed
time curl -si -X POST -H "Content-Type: application/json" \
  -d '{"title":"x'\'' AND SLEEP(3)--","body":"x"}' \
  http://localhost:8299/examples/notes
# → 201 in < 100ms  (SLEEP never ran)

Phase 3 — Path Traversal / LFI / PHP Wrappers

IDAttackExpectedv1.5.329
P-01../../etc/passwd404✅ BLOCKED
P-02URL-encoded %2e%2e%2f variants (5 forms)404✅ BLOCKED
P-03Double-encoded %252e%252e404✅ BLOCKED
P-04UTF-8 overlong %c0%ae404✅ BLOCKED
P-05php://input / php://filter / data://404✅ BLOCKED
P-06LFI via {id} parameter404✅ BLOCKED
P-07Null byte 1%00.html200 (int cast → 1)✅ SAFE — DB record for id=1 returned
P-08.htaccess on Apache403✅ BLOCKED
P-08b.htaccess on PHP built-in server200⚠️ EXPOSED (see VULN-01)
P-09.git/HEAD404✅ BLOCKED
P-10Backup files (.bak, .swp, ~, etc.)404✅ BLOCKED

Phase 4 — HTTP Protocol Attacks

IDAttackExpectedv1.5.329
H-01CL.TE request smugglingno response (PHP built-in blocks)
H-02TE.CL smuggling405 (root method mismatch)
H-03TE.TE obfuscated Transfer-Encodingno response
H-04HTTP/1.0 downgrade200 (correct body)
H-05Absolute URI proxy abuse404
H-06HTTP header folding500 (PHP built-in bug)⚠️ VULN-02
H-07HTTP pipeliningresponses interleaved✅ SAFE
H-08100 simultaneous custom headers200✅ SAFE
H-10WebSocket upgrade200 (upgrade ignored)✅ SAFE
H-12Invalid HTTP version (HTTP/9.9)200 (PHP built-in accepts)✅ SAFE

Phase 5 — Mass Assignment / IDOR / Business Logic

IDAttackExpectedv1.5.329
B-01Mass assignment (id, __proto__ in body)201 (extra fields ignored)✅ SAFE
B-02IDOR: DELETE another user's note204ℹ️ Expected (examples have no ownership)
B-04Negative / zero ID404✅ SAFE
B-05Integer overflow ID404✅ SAFE
B-06DELETE then re-access same ID404✅ SAFE
B-07Concurrent DELETE raceall 404 (idempotent)✅ SAFE
B-08Body at 1MB limit413✅ BLOCKED

Phase 6 — API Key Bypass

IDAttackExpectedv1.5.329
A-01No key401✅ BLOCKED
A-02Key in query string (?key=, ?api_key=)401✅ BLOCKED
A-03Key in request body401✅ BLOCKED
A-04Header name case variations200 (PSR-7 normalises)✅ SAFE
A-05Leading/trailing whitespace in value200 (PSR-7 trims)✅ SAFE
A-06//machine/health double slash401 without key, 200 with✅ SAFE
A-07X-Original-URL / X-Rewrite-URL200 (header ignored)✅ SAFE
A-08OPTIONS preflight bypass405✅ BLOCKED
A-09HEAD method401✅ BLOCKED
A-10Common password brute force401 all✅ BLOCKED
A-11URL-encoded path (%6Dachine)404✅ BLOCKED
bash
# Timing attack: hash_equals used → constant-time comparison
time (for i in $(seq 1 10); do
  curl -so /dev/null -H "X-NENE2-API-Key: a" http://localhost:8299/machine/health
done)
time (for i in $(seq 1 10); do
  curl -so /dev/null -H "X-NENE2-API-Key: pentest-key" http://localhost:8299/machine/health
done)
# → timing difference < 5ms over 10 requests: SAFE

Phase 7 — Injection / XSS / SSTI / Code Execution

IDAttackExpectedv1.5.329
I-01XSS <script>alert(1)</script> stored201, returned as JSON string✅ SAFE — JSON encoding neutralises
I-02SSTI 49 / ${7*7}201, stored literally✅ SAFE — no template engine
I-03PHP <?php system("id"); ?>201, stored as literal✅ SAFE
I-04Log4Shell ${jndi:ldap://...}200 (header ignored)✅ SAFE — PHP, not Java
I-051000-level nested JSON400 (PHP parse limit)✅ BLOCKED
I-06Unicode BiDi control characters201 (stored)✅ SAFE — display-only risk
I-07Duplicate JSON keyslast value wins (PHP behaviour)ℹ️ INFO-01

Stored XSS note: XSS payloads are stored and returned verbatim in JSON responses. Since the API is JSON-only (Content-Type: application/json + X-Content-Type-Options: nosniff), browsers will not execute the script. The risk only materialises if another application renders this data in an HTML context without escaping.

Phase 8 — Deserialisation / PHP Object Injection

IDAttackExpectedv1.5.329
D-01phar:// wrapper in path param404✅ BLOCKED
D-02PHP O:8:"stdClass":... serialize payload400 (invalid body)✅ BLOCKED
D-03URL-encoded form with serialize payload400 (wrong Content-Type)✅ BLOCKED

Phase 9 — Header Injection / Response Splitting

IDAttackExpectedv1.5.329
R-01Location header injection via created note id/examples/notes/<int>✅ SAFE — int-only
R-02CRLF in WWW-Authenticate via JWT errorsanitised fixed message✅ SAFE
R-03Content-Type sniffingX-Content-Type-Options: nosniff✅ SAFE
R-04ClickjackingX-Frame-Options: SAMEORIGIN✅ SAFE

Phase 10 — CORS / SOP Bypass

IDAttackExpectedv1.5.329
C-01Origin: null (sandboxed iframe)Vary: Origin, no ACAO header✅ SAFE
C-02CRLF in Origin headersanitised by curl/http layer✅ SAFE
C-03Vary header cache poisoningVary: Origin present✅ SAFE
C-04Preflight with injected methodmethod ignored by PHP✅ SAFE
C-05Access-Control-Allow-Origin: *header absent (allowlist empty)✅ SAFE

Phase 11-20 — Encoding / Protocol / Timing

IDAttackResult
E-01Emoji / high Unicode in JSON✅ 201 (stored correctly)
E-02BiDi RTL override (spoofing risk)✅ 201 (display-only)
E-05Pagination SQLi via query params✅ 422 (validated as integer)
H-06bFolded Authorization header⚠️ 500 (PHP built-in bug)
20X-Request-Id 129-char rejected✅ Server generates new random ID
21Log injection via X-Request-Id %0a✅ Rejected (invalid chars)
22Apache ServerTokens/ServerSignatureServer: Apache only
23JWT sub=admin privilege escalation✅ Claims not used for authz
26JWT replay (expired 2s ago)✅ 401 Token has expired.
27500 stack trace disclosure✅ Generic message only
28XSS in Problem Details instance✅ URL-encoded (safe)
29SSRF via health check endpoint✅ No URL accepted
15API key timing oraclehash_equals — < 5ms diff

3. Findings

VULN-01 — .htaccess readable from PHP built-in server ⚠️ MEDIUM

Trigger: curl http://localhost:8299/.htaccess
Response: 200 + full file content (Apache rewrite rules)
Root cause: PHP's built-in server (php -S) does not enforce .htaccess access restrictions — it treats .htaccess as a static file.
Impact: Reveals URL rewrite rules. The content is non-secret (no passwords/tokens), but confirms the rewrite-to-index-php pattern.
Mitigation: Use the Apache container (docker compose up -d app) instead of php -S for security-sensitive testing. Apache correctly returns 403.

bash
# Apache (correct): 403 Forbidden
curl -si http://localhost:8200/.htaccess | head -1

# PHP built-in server (exposed): 200 OK
curl -si http://localhost:8299/.htaccess | head -1

VULN-02 — HTTP header folding crashes PHP built-in server ⚠️ LOW

Trigger:

GET / HTTP/1.1\r\nHost: localhost\r\nX-NENE2-API-Key:\r\n <key>\r\n\r\n

Response: HTTP/1.0 500 Internal Server Error (empty body)
Root cause: PHP's built-in HTTP server does not support RFC 7230 header folding (deprecated but still valid in HTTP/1.1). NENE2 framework code is not involved.
Impact: Development-only (PHP built-in server). Apache handles folded headers correctly.

INFO-01 — Duplicate JSON keys: last value wins

{"title":"first","title":"INJECTED"}title = "INJECTED"
Standard PHP json_decode behaviour. Validation applies to the final (last) value, so there is no validation-bypass path. Noted for awareness.


4. Security Invariants Verified

These guarantees held across all 150+ test cases:

InvariantVerification
All SQL queries parameterisedSLEEP not executed; injection payloads stored as literals
JWT must be HS256 + valid sig + integer expAll 17 JWT attack variants blocked
API key checked with hash_equalsTiming difference < 5ms over 10 iterations
Content-Length overflow handled413 with correct headers, no PHP warning leak
Security headers on every responseCSP / XCTO / XFO / Referrer-Policy / Permissions-Policy confirmed
Server: / X-Powered-By: removedNeither header present in Apache responses
Stack traces never in 500 bodyOnly generic "The server encountered an unexpected condition."
Path traversal blockedAll 15 encoding variants return 404
.env / .git / backup filesAll 404 in document root
CORS default: no allowed originsAccess-Control-Allow-Origin absent for arbitrary origins

5. Running the Test Suite

Minimum viable repeat of the key checks (< 5 minutes):

bash
TARGET=http://localhost:8299
APIKEY=pentest-key
SECRET=pentest-jwt-secret-32chars-min!!
CID=$(docker ps --filter "publish=8299" --format "{{.ID}}")

# 1. JWT alg:none
H=$(echo -n '{"typ":"JWT","alg":"none"}' | base64 -w0 | tr '+/' '-_' | tr -d '=')
P=$(echo -n '{"sub":"admin","exp":9999999999}' | base64 -w0 | tr '+/' '-_' | tr -d '=')
curl -si -H "Authorization: Bearer $H.$P." $TARGET/examples/protected | grep "HTTP/"
# expected: 401

# 2. SQL injection time-based
time curl -so /dev/null -X POST -H "Content-Type: application/json" \
  -d '{"title":"x'\'' AND SLEEP(3)--","body":"x"}' $TARGET/examples/notes
# expected: < 500ms total

# 3. Path traversal
curl -si "$TARGET/%2e%2e/%2e%2e/etc/passwd" | grep "HTTP/"
# expected: 404

# 4. Content-Length overflow
curl -si -X POST -H "Content-Length: 9999999999999" $TARGET/ | head -3
# expected: 413 Request Entity Too Large (not 200 + PHP warning)

# 5. API key timing
time (for i in $(seq 1 10); do
  curl -so /dev/null -H "X-NENE2-API-Key: a" $TARGET/machine/health
done)
# expected: similar timing to correct key (hash_equals)

# 6. .htaccess exposure (Apache only)
curl -si http://localhost:8200/.htaccess | grep "HTTP/"
# expected: 403

# 7. JWT exp required
NEXP=$(docker exec $CID php -r "
  require 'vendor/autoload.php';
  \$v = new Nene2\Auth\LocalBearerTokenVerifier('$SECRET');
  echo \$v->issue(['sub'=>'user1']);
")
curl -si -H "Authorization: Bearer $NEXP" $TARGET/examples/protected | grep "detail"
# expected: "Token must contain a numeric exp claim."

Released under the MIT License.