# Security Tests > **Status**: produced by autodev `/test-spec` Phase 2 (2026-05-14). > **Naming**: post-rename target. Security tests focus on the JWT bearer + Authz boundary defined in AC-5 and AC-9. > **Out-of-scope (suite-tracked)**: the `iss` / `aud` validation gap (AC-5.3, CMMC L2 row 3, AZ-487 / AZ-494) is documented but NOT enforced today. Tests assert today's behaviour (AC-5.3 returns 200) — when the suite-wide remediation lands, update NFT-SEC-04. --- ### NFT-SEC-01: Missing Authorization header → 401 **Summary**: Verifies AC-5.4 — every protected endpoint rejects requests without an `Authorization` header. **Traces to**: AC-5.4 **Steps**: | Step | Consumer Action | Expected Response | |------|----------------|------------------| | 1 | `GET /vehicles` with no `Authorization` header | `401` | | 2 | `GET /missions` with no `Authorization` header | `401` | | 3 | `GET /missions/{any}/waypoints` with no `Authorization` header | `401` | | 4 | `POST /vehicles` with no `Authorization` header + valid body | `401` (no row written — verify via side-channel `count` unchanged) | **Pass criteria**: every protected endpoint returns 401; no DB side-effect. --- ### NFT-SEC-02: Invalid signature → 401 **Summary**: Verifies AC-5.5 — token signed with a different secret is rejected. **Traces to**: AC-5.5 **Steps**: | Step | Consumer Action | Expected Response | |------|----------------|------------------| | 1 | Mint token `T_bad` with `WRONG_SECRET=other-secret-32-chars-min!!!!!!`, otherwise valid (`exp = now + 1h`, `permissions=FL`) | | | 2 | `GET /vehicles` with `Authorization: Bearer T_bad` | `401` | **Pass criteria**: `401`. --- ### NFT-SEC-03: Expired token outside skew → 401; inside skew → 200 **Summary**: Verifies AC-5.6 + AC-5.2 (1-min skew tighter than .NET's 5-min default). **Traces to**: AC-5.2, AC-5.6 **Steps**: | Step | Consumer Action | Expected Response | |------|----------------|------------------| | 1 | Mint token `T_exp` with `exp = now - 120s` (outside 60s skew); `permissions=FL` | | | 2 | `GET /vehicles` with `Authorization: Bearer T_exp` | `401` | | 3 | Mint token `T_skew` with `exp = now - 30s` (inside 60s skew); `permissions=FL` | | | 4 | `GET /vehicles` with `Authorization: Bearer T_skew` | `200` | **Pass criteria**: `T_exp` rejected; `T_skew` accepted. --- ### NFT-SEC-04: Missing `iss` and `aud` claims accepted (today's behavior, AC-5.3) **Summary**: Verifies the `ValidateIssuer = false` and `ValidateAudience = false` configuration. This test will FAIL once the suite-wide remediation (AZ-487 / AZ-494) lands — that's good news; update the test then. **Traces to**: AC-5.3 **Steps**: | Step | Consumer Action | Expected Response | |------|----------------|------------------| | 1 | Mint token with NO `iss` and NO `aud` claim, valid signature + lifetime, `permissions=FL` | | | 2 | `GET /vehicles` with that token | `200` | **Pass criteria**: `200` today; will become `401` post-remediation. --- ### NFT-SEC-05: Missing `permissions` claim → 403 **Summary**: Verifies AC-5.8 — valid signature + lifetime is not enough; the `permissions=FL` claim is required. **Traces to**: AC-5.8, AC-9.1 **Steps**: | Step | Consumer Action | Expected Response | |------|----------------|------------------| | 1 | Mint token with no `permissions` claim, valid otherwise | | | 2 | `GET /vehicles` | `403` | **Pass criteria**: `403`. --- ### NFT-SEC-06: Wrong `permissions` claim value → 403 **Summary**: Verifies AC-9.2 — the policy is exact-string match, hardcoded. **Traces to**: AC-9.2 **Steps**: | Step | Consumer Action | Expected Response | |------|----------------|------------------| | 1 | Mint token with `permissions="ADMIN"`, valid otherwise | | | 2 | `GET /vehicles` | `403` | | 3 | Mint token with `permissions="fl"` (lowercase), valid otherwise | | | 4 | `GET /vehicles` | `403` | | 5 | Mint token with `permissions="FLight"`, valid otherwise | | | 6 | `GET /vehicles` | `403` | **Pass criteria**: `403` for every wrong-value case. --- ### NFT-SEC-07: Health endpoint exempt from auth **Summary**: Verifies AC-7.1, AC-9.4 (contrast) — `/health` is anonymous. **Traces to**: AC-7.1, AC-9.4 **Steps**: | Step | Consumer Action | Expected Response | |------|----------------|------------------| | 1 | `GET /health` with no `Authorization` | `200` | | 2 | `GET /health` with `Authorization: Bearer ` | `200` (auth not evaluated) | **Pass criteria**: `200` in both cases. --- ### NFT-SEC-08: Stack trace not leaked in 500 body **Summary**: Verifies AC-8.6 + AC-10.3 — internal exception details stay in the log, not the HTTP body. **Traces to**: AC-8.6, AC-10.3 **Steps**: | Step | Consumer Action | Expected Response | |------|----------------|------------------| | 1 | Force a `500` (drop `vehicles` table mid-test, then `GET /vehicles/{any}`) | | | 2 | Inspect response body | `body == { "statusCode":500, "message":"Internal server error" }` exactly; NO key matching `stack`, `stackTrace`, `exception`, `inner`, `trace`; NO file path; NO type name in the body | | 3 | `docker logs missions \| grep "Unhandled exception"` | At least one matching line; line contains the file path of the throw site OR the exception type name (the log-side info is private to operators) | **Pass criteria**: response body contains only `statusCode`, `message`; log contains stack info. --- ### NFT-SEC-09: SQL injection guard via parameterised queries **Summary**: Defensive — verifies linq2db's parameterised query path is in effect for filter strings. **Traces to**: AC-1.6 (filter), AC-2.3 (filter), defensive **Steps**: | Step | Consumer Action | Expected Response | |------|----------------|------------------| | 1 | `GET /vehicles?name='%20OR%20'1'%3D'1` (URL-encoded `' OR '1'='1`) | `200`; `body.length == 0` (no row matches the literal `' OR '1'='1` string against `BR-01` etc.) | | 2 | `GET /missions?name=%3B%20DROP%20TABLE%20vehicles%3B%20--` (URL-encoded `; DROP TABLE vehicles; --`) | `200`; `body.TotalCount == 0`; side-channel verifies `vehicles` table still exists | **Pass criteria**: filter inputs are treated as literal strings; no SQL execution; no DDL side-effect. --- ## Notes - Tests that drop tables (NFT-SEC-08) run in a per-class fixture that recreates the schema before subsequent tests. - The CMMC L2 row 3 (`iss` / `aud`) gap is acknowledged but NOT remediated in this Epic; NFT-SEC-04 documents today's permissive behavior so a future enforcement change is detected. - No fuzz testing today (recommended follow-up under a separate refactor cycle).