# Security Test Scenarios ## SEC-01: SQL Injection via Coordinate Parameters **Trigger**: GET /api/satellite/tiles/latlon?Latitude=1;DROP TABLE tiles--&Longitude=1&ZoomLevel=18 **Expected**: Request rejected or treated as invalid parameter **Pass criterion**: HTTP 400 or parameter parsing error; no database damage; tiles table intact ## SEC-02: Path Traversal in Tile Serving **Trigger**: GET /tiles/18/../../../etc/passwd **Expected**: Request rejected; no file outside tiles directory served **Pass criterion**: HTTP 404 or 400; response body does not contain system file content ## SEC-03: Oversized Region Request **Trigger**: POST /api/satellite/request with sizeMeters=999999999 **Expected**: Either rejected or handled without resource exhaustion **Pass criterion**: No OOM; no infinite processing; either error response or bounded processing ## SEC-04: Malformed JSON in Route Request **Trigger**: POST /api/satellite/route with invalid JSON body **Expected**: Parse error returned **Pass criterion**: HTTP 400; error message indicates parsing failure; no crash --- ## Cycle 2 — AZ-487 JWT validation baseline The pre-AZ-487 assumption "no authentication" is superseded by these scenarios. The SEC-01..SEC-04 scenarios above still hold (they probe input handling, not the auth layer), but every authenticated test variant must now attach a Bearer token. ## SEC-05: Anonymous Request to Any Authenticated Endpoint Returns 401 **Trigger**: GET `/api/satellite/tiles/latlon?Latitude=...&Longitude=...&ZoomLevel=18` (or any `/api/satellite/*` endpoint) with NO `Authorization` header. **Precondition**: API running with `JWT_SECRET` configured. **Expected**: HTTP 401 Unauthorized; `WWW-Authenticate: Bearer` header present; response body does not leak validation internals. **Pass criterion**: status == 401 AND `WWW-Authenticate` header starts with `Bearer`. **AC trace**: AZ-487 AC-1. ## SEC-06: Expired Token Returns 401 **Trigger**: Same request as SEC-05 carrying a JWT signed with the configured secret but with `exp` in the past (clock-skew margin already exceeded). **Expected**: HTTP 401; the failure reason surfaces via `WWW-Authenticate` (e.g. `error="invalid_token"`, `error_description="The token is expired"`), never in the response body. **Pass criterion**: status == 401 AND response body does not contain `Expires:` / `NotBefore:` / stack traces / internal exception types. **AC trace**: AZ-487 AC-2. ## SEC-07: Tampered Signature Returns 401 **Trigger**: Same request as SEC-05 carrying a JWT whose payload was modified after signing so the HMAC no longer verifies. **Expected**: HTTP 401; body free of cryptographic detail. **Pass criterion**: status == 401 AND request never reaches downstream handlers (no DB write, no Google Maps fetch). **AC trace**: AZ-487 AC-3. ## SEC-08: Startup Fails on Missing / Short Secret **Trigger**: Boot the API container with `JWT_SECRET` unset, empty, or shorter than 32 bytes. **Observable**: Container exit code, stdout error message. **Pass criterion**: container exits non-zero within 10s of start; stderr contains a message identifying the missing or short `JWT_SECRET` and the 32-byte minimum; Kestrel never binds to its port. **AC trace**: AZ-487 AC-5. Behavioral test — no input data. ## SEC-09: Valid Token Reaches Handler Unchanged **Trigger**: GET `/api/satellite/tiles/latlon?...` with a JWT signed by the configured secret and `exp` in the future. **Expected**: Response is byte-identical (status, body, headers other than `Authorization`/`WWW-Authenticate`) to the pre-AZ-487 baseline for the same parameters. **Pass criterion**: status == 200 AND response body matches BT-01 expected schema. **AC trace**: AZ-487 AC-4. Also exercised by AZ-487 AC-8 / integration smoke parity. --- ## Cycle 2 — AZ-488 UAV upload authorization ## SEC-10: Valid Token Without GPS Permission Returns 403 on UAV Upload **Trigger**: POST `/api/satellite/upload` carrying a JWT with `permissions: ["FL"]` (no `GPS`); body is an otherwise-valid 1-item batch. **Precondition**: AZ-487 in place; AZ-488 endpoint registered with the `GPS` permission policy. **Expected**: HTTP 403 Forbidden; no row in `tiles`; no file under `./tiles/uav/`. **Pass criterion**: status == 403 AND `SELECT COUNT(*) FROM tiles WHERE source='uav' AND ...` == pre-test count AND uploaded file does NOT exist on disk. **AC trace**: AZ-488 AC-6. ## SEC-11: Reject-Reason Details Do Not Leak Server Internals **Trigger**: POST `/api/satellite/upload` with a batch where item-1 deliberately fails Rule 5 (`IMAGE_TOO_UNIFORM`) and item-2 deliberately fails Rule 1 (`INVALID_FORMAT`). **Precondition**: Authenticated request with `GPS` permission. **Expected**: HTTP 200 with per-item results; each `rejectDetails` is short, human-readable, and contains none of: server-side file paths, exception type names, stack traces, internal class names, secrets, or hostnames. **Pass criterion**: For every rejected item, `rejectDetails` matches `^[A-Za-z0-9 .,()<>=:%/-]{0,200}$` AND contains no path separator (`/` or `\`) followed by a directory name from the server image (`tiles`, `src`, `obj`, `bin`). **AC trace**: AZ-488 § Security NFR. ## SEC-12: Wrong `iss` Claim Returns 401 **Trigger**: Same request as SEC-05 carrying a JWT signed with the configured secret, with valid `exp` / `nbf` / signature, and with an `aud` claim matching `JWT_AUDIENCE` — but with `iss` set to `https://wrong-issuer.invalid/` (not equal to `JWT_ISSUER`). **Precondition**: AZ-494 in place; API started with `JWT_ISSUER` + `JWT_AUDIENCE` env vars both populated (fail-fast contract). **Expected**: HTTP 401 Unauthorized; no handler reached; no leaked detail in body. **Pass criterion**: status == 401 AND response body contains no `iss` / `aud` value or internal exception detail. **AC trace**: AZ-494 AC-1. ## SEC-13: Wrong `aud` Claim Returns 401 **Trigger**: Same request as SEC-05 carrying a JWT signed with the configured secret, with valid `exp` / `nbf` / signature, and with `iss` matching `JWT_ISSUER` — but with `aud` set to `wrong-audience-not-satellite` (not equal to `JWT_AUDIENCE`). **Precondition**: AZ-494 in place; API started with `JWT_ISSUER` + `JWT_AUDIENCE` env vars both populated. **Expected**: HTTP 401 Unauthorized; no handler reached; no leaked detail in body. **Pass criterion**: status == 401 AND response body contains no `iss` / `aud` value or internal exception detail. **AC trace**: AZ-494 AC-2.