This commit captures everything produced during autodev existing-code Steps 1 (Document), 2 (Architecture Baseline Scan), and 3 (Test Spec), together with the targeted auth + CORS re-sync triggered on 2026-05-14 when codebase drift was detected at Step 4 entry. None of this work was previously committed. Step 1 (Document) — 50+ _docs/02_document/ files: problem, solution, architecture, system flows, glossary, module-layout, per-component specs (01..06), modules, deployment, diagrams, data model, FINAL report, verification log, discovery. Step 2 (Architecture Baseline) — architecture_compliance_baseline.md. Verdict PASS_WITH_WARNINGS (0 Critical, 0 High, 1 Medium, 2 Low). No High/Critical findings; auto-chained to Step 3 per existing-code flow. Step 3 (Test Spec) — _docs/02_document/tests/* (67 scenarios across blackbox, security, resilience, resource-limit, performance), plus e2e/docker-compose.test.yml, e2e/seed/run.sh, scripts/run-tests.sh, scripts/run-performance-tests.sh. Coverage 88% over the active scope (40 of 45 items covered, 6 RB-deferred, 5 documented-as-uncovered). Targeted auth + CORS re-sync — replaces the deleted in-house token issuer with a JWKS-verifier model. AuthController and TokenService removed; JwtExtensions switched from HS256 symmetric to ES256 over admin's JWKS. ConfigurationResolver and CorsConfigurationValidator added under src/Infrastructure/. ADR-002 and ADR-006 retired; SEC-01, SEC-02, SEC-03 marked Closed. One new testability risk recorded in architecture.md Open Risks Section 6 (JWKS HTTPS gating). Source changes: - src/Auth/JwtExtensions.cs (modified) — ES256, JWKS, alg pinning - src/Program.cs (modified) — DI wiring for ConfigurationResolver and CorsConfigurationValidator - src/Controllers/AuthController.cs (deleted) — no in-service issuance - src/Services/TokenService.cs (deleted) — same - src/Infrastructure/ConfigurationResolver.cs (new) - src/Infrastructure/CorsConfigurationValidator.cs (new) - .env.example (new) — required env var documentation - .gitignore (updated) Cross-repo coordination: _docs/cross-repo/flights_h1_h2_h3_change_spec captures the change-spec for downstream services that consumed the now deleted /auth endpoints. Co-authored-by: Cursor <cursoragent@cursor.com>
7.3 KiB
Security Tests
Blackbox-level only. Code-level vulnerabilities (e.g., SQL injection at the source level) are out of scope here — they belong to Step 14 (Security Audit). The SEC-XX gap list in
security_approach.mdis the broader inventory; the tests below are the ones that can be exercised through the public surface.Auth model assumed by these tests: annotations is a verifier-only service. Tokens are minted by the e2e harness's mock issuer (see
test-data.md→ "Bearer token harness") using an ES256 key pair whose public half is published at the JWKS URL the service fetches at boot. There is no/auth/login,/auth/refresh, or/auth/registerendpoint on this service.
NFT-SEC-01: JWT signature mismatch
Summary: A token signed with a key not published in the SUT's JWKS is rejected. Traces to: AC-F-50, AC-F-52
Steps:
| Step | Consumer Action | Expected Response |
|---|---|---|
| 1 | Mint an ES256 token with valid iss / aud / exp and an ANN claim, but signed with a private key whose public half is not in the JWKS |
token is well-formed |
| 2 | POST /annotations with that bearer token |
HTTP 401; error envelope; error.code matches `/auth |
Pass criteria: 401, no 500, no leaking which key the SUT expected. Duration: 3s.
NFT-SEC-02: JWT expired
Summary: An expired ES256 JWT is rejected. Traces to: AC-F-50
Steps:
| Step | Consumer Action | Expected Response |
|---|---|---|
| 1 | Mint a JWT with exp 1 hour in the past, signed with a key in the JWKS, otherwise valid (iss, aud, ANN claim) |
token is well-formed |
| 2 | POST /annotations with that bearer token |
HTTP 401; error envelope |
Pass criteria: 401; SUT does not honor the expired token. Duration: 3s.
NFT-SEC-03: Cross-policy attempt — DATASET token cannot create annotations
Summary: Policy DATASET cannot reach /annotations POST.
Traces to: AC-F-52
Steps:
| Step | Consumer Action | Expected Response |
|---|---|---|
| 1 | Mint an ES256 token with the DATASET claim and ANN claim absent |
token is well-formed |
| 2 | POST /annotations with that bearer |
HTTP 403; error envelope |
Pass criteria: 403, request rejected before any DB / FS write. Duration: 3s.
NFT-SEC-04: Cross-policy attempt — ANN token cannot mutate settings
Summary: Policy ANN cannot reach an [Authorize(Policy = "ADM")] route.
Traces to: AC-F-52
Steps:
| Step | Consumer Action | Expected Response |
|---|---|---|
| 1 | PUT /settings/system with an ES256 token carrying only the ANN claim |
HTTP 403; error envelope |
Pass criteria: 403. Duration: 3s.
NFT-SEC-05: Anonymous access to non-public endpoints
Summary: Every endpoint other than /health requires authentication.
Traces to: AC-F-50, AC-F-52, security_approach.md surface inventory
Steps:
| Step | Consumer Action | Expected Response |
|---|---|---|
| 1 | GET /annotations with no Authorization |
HTTP 401 |
| 2 | GET /dataset with no Authorization |
HTTP 401 |
| 3 | GET /classes with no Authorization |
HTTP 401 |
| 4 | GET /settings/system with no Authorization |
HTTP 401 |
| 5 | GET /health with no Authorization |
HTTP 200 |
Pass criteria: every authenticated endpoint returns 401; only /health is anonymous.
Duration: 5s.
NFT-SEC-06: Error envelope leaks no stack trace in production-mode env
Summary: Triggering a 500 path returns an error envelope with no stackTrace / innerException fields.
Traces to: AC-N-04
Preconditions: SUT started with ASPNETCORE_ENVIRONMENT=Production.
Steps:
| Step | Consumer Action | Expected Response |
|---|---|---|
| 1 | Trigger a path that produces a 500 (e.g., NFT-RES-03 step 2 in DB-paused state) OR a malformed multipart body | HTTP 5xx; body is the error envelope |
| 2 | Inspect body | no key matches /stack/i, /inner/i, /trace/i (case-insensitive) |
Pass criteria: error envelope present; no stack-trace leakage. Duration: 30s (depends on fault induction).
NFT-SEC-07: Path traversal in image / thumbnail GET routes
Summary: Path traversal sequences in the id segment do not escape images_dir.
Traces to: implicit; SEC-05 broader scope
Steps:
| Step | Consumer Action | Expected Response |
|---|---|---|
| 1 | GET /annotations/%2E%2E%2Fetc%2Fpasswd/image (encoded ../etc/passwd) |
HTTP 400 OR HTTP 404 (NOT 200, NOT containing /etc/passwd content) |
| 2 | GET /annotations/..%2F..%2Fetc%2Fpasswd/thumbnail |
same |
Pass criteria: SUT rejects or returns 404; no host file content in the response body. Duration: 5s.
NFT-SEC-08: Token claim modification (signature breaks)
Summary: An attacker who edits a JWT payload to elevate to ADM but cannot resign sees 401, not 200. Traces to: AC-F-52
Steps:
| Step | Consumer Action | Expected Response |
|---|---|---|
| 1 | Mint an ES256 token with the ANN claim |
original token |
| 2 | Decode payload; replace policy claim with ADM; re-encode payload but keep the original signature |
tampered token |
| 3 | PUT /settings/system with the tampered token |
HTTP 401; error envelope |
Pass criteria: 401 — signature validation catches the tamper. Duration: 5s.
NFT-SEC-09: CORS preflight respects configured allow-list
Summary: With the SUT booted under ASPNETCORE_ENVIRONMENT=Production and CorsConfig:AllowedOrigins=["https://app.azaion.local"], a preflight from an arbitrary origin is not given a wildcard ACAO header. CorsConfigurationValidator already prevents the wide-open default in Production.
Traces to: AC-N-CORS (see restrictions.md ENV-06)
Steps:
| Step | Consumer Action | Expected Response |
|---|---|---|
| 1 | OPTIONS /annotations with Origin: https://attacker.example, Access-Control-Request-Method: POST |
HTTP 204 with no Access-Control-Allow-Origin: *; either no ACAO at all, or an ACAO matching the configured allow-list (which the attacker origin is not in) |
| 2 | OPTIONS /annotations with Origin: https://app.azaion.local |
HTTP 204; Access-Control-Allow-Origin: https://app.azaion.local |
Pass criteria: only configured origins receive a permissive ACAO; arbitrary origins do not. Duration: 3s.
NFT-SEC-10: Algorithm confusion — alg=HS256 over the public ES256 key
Summary: Annotations pins ValidAlgorithms = [EcdsaSha256] to block the classic JWKS-confusion attack where an attacker forges an HS256 token using the published ES256 public key as the HMAC secret.
Traces to: AC-F-50
Steps:
| Step | Consumer Action | Expected Response |
|---|---|---|
| 1 | Fetch the SUT's published JWKS; export the ES256 public key bytes | bytes obtained |
| 2 | Mint a JWT with alg=HS256 and the public key bytes as the HMAC key, with otherwise-valid iss / aud / exp / ANN claim |
forged token |
| 3 | GET /annotations with that bearer token |
HTTP 401; error envelope |
Pass criteria: 401 — algorithm pinning rejects the forged token. Duration: 5s.