Security Testing (OWASP & DAST)ΒΆ
ContextForge includes a two-layer security test suite focused on OWASP A01:2021 β Broken Access Control. The layers are independent: Layer 1 runs in every CI environment with no extra infrastructure; Layer 2 requires a running OWASP ZAP daemon.
Testing Pyramid PlacementΒΆ
| Layer | Tool | Marker | Requires |
|---|---|---|---|
| Layer 1 β direct access control | Playwright / pytest | owasp_a01 | Gateway only |
| Layer 2 β ZAP DAST | OWASP ZAP + pytest | owasp_a01_zap | make testing-up |
Layer 1 β Direct Access Control TestsΒΆ
Location: tests/playwright/security/owasp/test_a01_broken_access_control.py
These tests call the gateway's REST API directly via Playwright's APIRequestContext. No browser, no proxy, no ZAP.
| Attack pattern | CWE | What is checked |
|---|---|---|
| Force browsing | CWE-284, CWE-862 | 7 protected endpoints return 401 for anonymous requests |
| IDOR / cross-user | CWE-639 | User A token cannot read User B's private resources |
| Cross-tenant | CWE-639, CWE-285 | Team-A-scoped token cannot see Team-B resources |
| Vertical privilege escalation | CWE-269, CWE-285 | Non-admin tokens are rejected by admin-only APIs |
| JWT tampering | CWE-345, CWE-287 | Unsigned, payload-modified, expired, alg=none, wrong iss/aud |
| HTTP method access control | CWE-284 | Non-admin cannot mutate publicly readable resources |
| CORS enforcement | CWE-942 | No wildcard or reflected Access-Control-Allow-Origin for arbitrary origins |
Running Layer 1ΒΆ
The target defaults to http://localhost:8080. Override with:
Layer 2 β ZAP DAST IntegrationΒΆ
Location: tests/playwright/security/owasp/test_a01_zap_dast.py
ZAP acts as an active scanner. The test suite:
- Seeds ZAP's site tree by directly accessing each protected path (
zap.core.access_url) β necessary because ZAP's traditional spider follows HTML hyperlinks and cannot discover REST API endpoints on its own. - Runs ZAP's traditional spider to catch any additional HTML/UI paths.
- Waits for the passive scan queue to drain, then asserts no HIGH/CRITICAL A01 alerts.
- Runs the active scan (attack payloads) and asserts no CRITICAL A01 alerts.
- Writes a JSON report to
tests/reports/.
PrerequisitesΒΆ
Start the testing stack and the ZAP DAST daemon:
make testing-up # gateway + nginx + Locust + test servers
make testing-zap-up # OWASP ZAP daemon (separate profile)
ZAP runs in its own dast Docker Compose profile to avoid pulling the heavyweight image and reserving memory during normal test runs. Wait for it to become healthy β ZAP's JVM takes 30β45 seconds to start.
Running Layer 2ΒΆ
This sets the required environment variables automatically and runs only the owasp_a01_zap marker. To run manually:
ZAP_BASE_URL=http://localhost:8090 \
ZAP_API_KEY=changeme \
ZAP_TARGET_URL=http://host.docker.internal:8080 \
pytest tests/playwright/security/owasp/ -v -m owasp_a01_zap
Environment VariablesΒΆ
| Variable | Default | Description |
|---|---|---|
ZAP_BASE_URL | (unset β skips ZAP tests) | ZAP API daemon URL, host-visible (e.g. http://localhost:8090) |
ZAP_API_KEY | changeme | ZAP API key configured in docker-compose |
TEST_BASE_URL | http://localhost:8080 | Gateway URL, host-visible (used for preflight health check) |
ZAP_TARGET_URL | http://host.docker.internal:8080 | Gateway URL as seen from inside the ZAP container |
Why two URL variables?
TEST_BASE_URL is the host-visible address used by the Python test process for health checks and result verification. ZAP_TARGET_URL is the address ZAP itself uses when spidering and scanning β inside Docker, localhost resolves to the ZAP container, not the host. host.docker.internal:8080 is the correct Docker-to-host address on macOS and Windows. On Linux, use the host's Docker bridge IP (typically 172.17.0.1).
AuthenticationΒΆ
ZAP authenticates automatically. At startup the zap fixture:
- Generates an admin JWT using the application's own
create_jwt_tokenutility (teams=None+is_admin=Trueβ admin bypass scope). - Installs it as a permanent
Authorization: Bearer <token>header on all ZAP outbound requests via ZAP's Replacer add-on.
No manual login or session configuration is required.
Scan ResilienceΒΆ
The test suite imports the full OpenAPI spec (300+ paths) into ZAP. The default Docker memory limit is 16 GB. The passive scan completes reliably at this limit and covers the full API surface. The active scan (attack payloads against every endpoint) may time out or OOM on very large APIs and is skipped gracefully when this happens:
- If ZAP disconnects mid-scan, the polling loop breaks and proceeds with partial results.
- After the scan, the test reconnects via a fresh ZAP client (ZAP restarts automatically via
restart: unless-stopped). - If ZAP is still unreachable after reconnecting, the test is skipped with a message indicating the memory limit.
ReportsΒΆ
JSON alert reports are written to tests/reports/ after each scan phase:
| File pattern | Contents |
|---|---|
zap_a01_passive_failures_<ts>.json | HIGH/CRITICAL A01 alerts from passive scan |
zap_a01_active_critical_<ts>.json | CRITICAL A01 alerts from active scan |
zap_a01_full_report_<ts>.json | All A01 alerts regardless of severity |
Skipping ZAP Tests in CIΒΆ
ZAP tests are skipped automatically when ZAP_BASE_URL is not set. Standard CI runs (make test) do not set this variable, so only Layer 1 runs. Enable Layer 2 in CI by adding a ZAP daemon service to your pipeline and setting ZAP_BASE_URL before running make test-zap.