Files
satellite-provider/_docs/02_document/tests/security-tests.md
T
Oleksandr Bezdieniezhnykh 34ee1e0b83 [AZ-808] [AZ-811] Strict validation on region POST + lat/lon GET
AZ-808: FluentValidation for POST /api/satellite/request
- RegionRequestValidator: id non-empty, lat/lon/sizeMeters/zoomLevel ranges
- RequestRegionRequest: [JsonRequired] on every property, no implicit defaults
- Wired via .WithValidation<RequestRegionRequest>() in MapPost chain
- Unit + integration tests + curl probe script
- New contract: contracts/api/region-request.md v1.0.0

AZ-811: FluentValidation + envelope filter for GET /api/satellite/tiles/latlon
- GetTileByLatLonQuery: nullable record (double?/int?) so the minimal-API
  binder never short-circuits with BadHttpRequestException before filters
- GetTileByLatLonQueryValidator: Cascade(Stop) + NotNull + InclusiveBetween
  per param; missing surfaces as `\`<name>\` is required.`
- RejectUnknownQueryParamsEndpointFilter: reusable IEndpointFilter that
  rejects any query key outside the allowed set with errors[<key>] map;
  catches legacy `?Latitude=` typos and hostile probes (`?debug=1&admin=1`)
- Handler: [AsParameters] GetTileByLatLonQuery + .Value deref post-validator
- Unit (validator + filter) + integration tests + curl probe script
- New contract: contracts/api/tile-latlon.md v1.0.0

Shared hygiene
- Promote AssertErrorsContainsMention from per-test-file private helpers to
  ProblemDetailsAssertions (closes batch-1 Low-severity DRY warning)
- Sync Swagger param descriptions, README, blackbox/security/perf scripts,
  uuidv5 doc with the new lat/lon/zoom query-param names

Docs
- system-flows.md F1/F2 reference the new contracts + validation layers
- modules/api_program.md adds Api/Validators + Api/DTOs sections
- _autodev_state.md: batch 2 of 4 complete; next batch = AZ-809

All smoke tests green (mode=smoke, exit 0). AZ-808 + AZ-811 transitioned
to In Testing on Jira.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-22 16:29:41 +03:00

6.3 KiB

Security Test Scenarios

SEC-01: SQL Injection via Coordinate Parameters

Trigger: GET /api/satellite/tiles/latlon?lat=1;DROP TABLE tiles--&lon=1&zoom=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?lat=...&lon=...&zoom=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.