Files
annotations/_docs/02_document/tests/security-tests.md
T
Oleksandr Bezdieniezhnykh 03f879206e docs+src: complete Steps 1-3 outcomes + auth re-sync baseline
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>
2026-05-14 20:19:05 +03:00

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.md is 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/register endpoint 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.