mirror of
https://github.com/azaion/annotations.git
synced 2026-06-21 13:31:06 +00:00
03f879206e
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>
180 lines
7.3 KiB
Markdown
180 lines
7.3 KiB
Markdown
# 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|unauthor/i` |
|
|
|
|
**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.
|