Skip to content

DAST Baseline — OWASP ZAP

Scan date: 2026-07-20 Tool: OWASP ZAP baseline scan (zaproxy/zap-stable, zap-baseline.py) Prepared for: CSUB ITS security vendor review (US-44 / PR-F2)

Target

The story asks for a scan "against staging." No staging tier exists — the only environment axis this app has is WP_ENV ∈ {dev, production} (backend/app/config.py), and the only deployed host is demo.greaterangelssoftware.com, behind a Cloudflare tunnel (docs/diagrams/deployment/pass59_k8s_topology.puml). Scanning that live public host was ruled out without a separate explicit go-ahead. Instead this scan targets the local docker-compose.yml stack — the same images the real deploy path builds (docker/api.Dockerfile, docker/frontend.Dockerfile — US-48), composed the same way, reachable unauthenticated the way any external caller would be.

That substitution has a real gap, stated plainly rather than implied away: the compose stack runs in dev mode (WP_ENV unset), so HTTPSRedirectMiddleware, the session cookie's Secure flag, and the HSTS header are all off in what this scan actually saw — those three are gated on Settings.is_production (backend/app/config.py) and are covered instead by test_web_session_hardening_bdd.py's dedicated production-configured app fixture, not by this scan. TLS termination and the Cloudflare tunnel edge are also outside what a compose-stack scan can exercise. See docs/diagrams/deployment/pass53_dast_scan_topology.puml for the full picture of what this scan does and does not stand in for.

Reproduce with make security-dast (brings up an ephemeral copy of the compose stack, runs the baseline scan against it, tears the stack down).

Findings fixed during this baseline

Six findings were fixed as part of producing this baseline, verified by re-running the scan afterward and confirming each disappeared:

  1. Server Leaks Version Information (Server: nginx/1.27.5) — both nginx configs now set server_tokens off; (docker/frontend.nginx.conf, docker/compose-edge.nginx.conf).
  2. CSP: Failure to Define Directive with No Fallbackbase-uri and form-action have no default-src fallback per spec and were never named explicitly. Added to all three CSP-setting locations: docker/frontend.nginx.conf ('self'/'self'), the backend's default CSP (backend/app/middleware.py, 'none'/'none'), and the mock-IdP page's own CSP override (backend/app/routers/auth.py, 'self'/'self').
  3. Permissions-Policy Header Not Set — added geolocation=(), microphone=(), camera=() to both the backend (backend/app/middleware.py) and the frontend (docker/frontend.nginx.conf).
  4. Cross-Origin-Opener-Policy Header Missing — added same-origin to both.
  5. Cross-Origin-Resource-Policy Header Missing — added same-origin to both.
  6. Cross-Origin-Embedder-Policy Header Missing or Invalid (PASS-221) — added require-corp to both. This baseline had previously accepted the finding: require-corp would have blocked the Google Fonts stylesheet index.html linked to, unless Google's CDN sent a matching Cross-Origin-Resource-Policy header, which was never verified. PASS-221 self-hosts those fonts under frontend/public/fonts/ and removed the Google Fonts allowances from docker/frontend.nginx.conf's CSP — style-src, font-src, and connect-src now name no outside origin at all, so there is no remaining cross-origin fetch left for require-corp to break. The one other candidate — the per-theme, admin-configurable logoUrl (ChallengeBuilder.tsx) rendered as an <img> (Passport.tsx) — is already unreachable regardless of COEP: img-src has always been 'self' data:, so a campus-configured external logo URL was already blocked by CSP before this change and stays that way after it.

Triage

# Alert Risk Disposition Rationale
1 Absence of Anti-CSRF Tokens Medium Accepted Found on /mock-idp/login's POST form only — the dev-only mock IdP stand-in, which 404s whenever WP_AUTH_PROVIDER is not mock (backend/app/config.py refuses to boot in production with it set at all — SEC-5, docs/hecvat-lite.md). A real campus deployment never serves this route, and the "attacker" already controls the browser session establishing an identity — there is no cross-site victim to protect.
2 CSP: script-src unsafe-inline Medium Accepted Same /mock-idp/login page's own CSP override, needed for its inline onclick preset buttons. Same dev-only rationale as #1.
3 CSP: style-src unsafe-inline Medium Accepted The SPA's production CSP (docker/frontend.nginx.conf) needs it for dynamic per-theme CSS custom properties applied via inline style attributes (US-60 design-of-record token realignment). Removing it needs auditing every dynamic-style usage across the React app — deferred as tracked frontend work rather than attempted blind against a mobile-first, actively-themed UI.
4 Sub Resource Integrity Attribute Missing Medium Accepted Scripts/styles are served same-origin from the exact image the deploy pipeline builds (US-48), not a third-party CDN — SRI's threat model (a compromised third-party host swapping the file) does not apply; the same pipeline that builds the HTML also builds the JS/CSS it references.
5 Base64 Disclosure Informational Accepted Base64 content in built JS/CSS bundles (icon/font data URIs) and in /auth/acs's opaque signed session token — expected content, not a cleartext secret leak.
6 Modern Web Application Informational Accepted ZAP noting the app is a modern SPA; an observation about crawl strategy, not a finding.
7 Non-Storable Content Informational Accepted Some responses (/auth/session when unauthenticated) are correctly non-cacheable — expected, not a defect.
8 Sec-Fetch-Dest Header is Missing Informational Accepted Sec-Fetch-* are request headers modern browsers add automatically; ZAP's own scanner does not send them, so their "absence" here reflects the scanning tool, not anything the server could add to the browser's outgoing request.
9 Sec-Fetch-Mode Header is Missing Informational Accepted Same as #8.
10 Sec-Fetch-Site Header is Missing Informational Accepted Same as #8.
11 Sec-Fetch-User Header is Missing Informational Accepted Same as #8.
12 Session Management Response Identified Informational Accepted ZAP confirming it found a session-establishing response (Set-Cookie on /auth/acs) — the expected behavior of a working sign-in flow.
13 Storable and Cacheable Content Informational Accepted Static assets (icons, manifest, JS/CSS bundles) are cacheable by design — the PWA offline story (US-6) depends on service-worker + browser caching of exactly these. No session or student data appears in these responses.
14 User Controllable HTML Element Attribute (Potential XSS) Informational Accepted The returnTo query parameter is reflected into the dev-only mock-IdP page's hidden input value, already HTML-escaped server-side (_html_escape, backend/app/routers/auth.py) before interpolation — confirmed-mitigated; flagged informational because ZAP's static analysis cannot see past the escaping to confirm it itself.

No High or Critical findings. No unresolved-and-unaccepted findings remain — every row above is either fixed (the six listed above) or Accepted with a stated rationale.

Residual gaps

  1. No staging tier existed at the time of this scan (2026-07-20), and the compose stack does not reproduce WP_ENV=production's TLS/cookie/HSTS posture or the Cloudflare tunnel edge — see the Target section above and docs/diagrams/deployment/pass53_dast_scan_topology.puml. US-38 / PR-D1 (after this scan) has since stood up a staging tier (k8s-staging/, wellness-passport-staging — see docs/diagrams/deployment/pass45_staging_topology.puml), but this baseline has not been re-run against it — that re-scan remains outstanding, not implied by staging merely existing now.
  2. The live public host was never scanned. Scanning demo.greaterangelssoftware.com directly needs an explicit go-ahead this story did not seek.
  3. This is a baseline (passive) scan, not an authenticated active scan. ZAP's crawler reached only unauthenticated, publicly linked routes; it did not sign in and probe the admin/student API surface for injection-class defects. The authorization surface itself is covered instead by backend/tests/test_vendor_security_package_bdd.py's Scenario 4 (every admin route rejects a student session) and FR-A4's existing sweep — a different technique for a different class of defect than DAST covers.