mirror of
https://github.com/azaion/satellite-provider.git
synced 2026-06-21 11:41:14 +00:00
5e056b2334
Third concrete child of AZ-795 (cycle 8 batch 3). FluentValidation +
[JsonRequired] + UnmappedMemberHandling.Disallow combine to reject every
malformed payload at the API boundary with RFC 7807 ValidationProblemDetails.
Validators (SatelliteProvider.Api/Validators/, all new)
- CreateRouteRequestValidator: id non-empty, name/description length,
regionSizeMeters/zoomLevel ranges, points count [2, 500], cross-field
createTilesZip => requestMaps. Chains RoutePointValidator (per-point)
and GeofencePolygonValidator (per-polygon, guarded by When(Geofences != null)).
OverridePropertyName("geofences.polygons") on the geofences chain so
FluentValidation's default leaf-only key policy doesn't drop the parent
path on deep expressions like req.Geofences!.Polygons.
- RoutePointValidator: lat/lon ranges; OverridePropertyName("lat"/"lon")
chained AFTER InclusiveBetween (the extension is defined on
IRuleBuilderOptions<T, TProperty>, so the generic type is only
inferable after the first concrete rule) so error keys match the
wire format (`points[i].lat`) rather than the C# property name
(`points[i].latitude`).
- GeofencePolygonValidator: per-corner range checks via private nested
GeoCornerValidator; cross-field NW.Lat > SE.Lat and NW.Lon < SE.Lon
invariants emit at errors["geofences.polygons[i].northWest"].
DTOs (SatelliteProvider.Common/DTO/, [JsonRequired] additions only)
- CreateRouteRequest: id, name, regionSizeMeters, zoomLevel, points,
requestMaps, createTilesZip
- RoutePoint: Latitude, Longitude
- GeofencePolygon: NorthWest, SouthEast; Geofences: Polygons
- GeoPoint: Lat, Lon
Tests
- Unit: 26 methods total — 16 in CreateRouteRequestValidatorTests, 6 in
GeofencePolygonValidatorTests, 4 in RoutePointValidatorTests. Each
RuleFor/RuleForEach chain has at least one positive + one negative case.
- Integration: CreateRouteValidationTests.cs — 16 methods (happy + 15
failure modes) wired into smoke + full suites. Covers empty body,
missing/zero id, empty name, out-of-range regionSizeMeters/zoomLevel,
points count < 2, per-point lat/lon out-of-range, geofence invariants,
missing requestMaps, cross-field createTilesZip, unknown root field,
nested type mismatch.
- Manual probe: scripts/probe_route_validation.sh curl-exercises every
failure mode end-to-end + happy path.
Docs
- New contract _docs/02_document/contracts/api/route-creation.md v1.0.0
with nested DTO chain, invariants, per-field test cases table, and
advisories on the legacy service-layer RouteValidator + the
input/output RoutePoint vs RoutePointDto naming asymmetry.
- system-flows.md F4 sequence diagram extended with the validation-filter
branch; preconditions + error scenarios reference the new contract.
- modules/api_program.md: CreateRoute handler section added; Api/Validators
bumped to AZ-808/AZ-809/AZ-811.
- modules/common_dtos.md: DTO descriptions updated with [JsonRequired]
annotations and constraint summaries.
- tests/blackbox-tests.md BT-06/BT-N03/BT-N04/BT-N05 align with the new
wire format and named error keys.
- tests/security-tests.md SEC-04 references GlobalExceptionHandler's
JsonException branch + AZ-353 correlationId.
- _docs/03_implementation/batch_03_cycle8_report.md + reviews/batch_03_cycle8_review.md
(PASS_WITH_NOTES — F1 Low: OverridePropertyName documented inline,
F2 + F3 Info: pre-existing advisories for follow-up).
Smoke green (mode=smoke, exit 0). AZ-809 transitioned to In Testing on Jira.
Task file moved to _docs/02_tasks/done/.
Co-authored-by: Cursor <cursoragent@cursor.com>
105 lines
6.6 KiB
Markdown
105 lines
6.6 KiB
Markdown
# 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 (truncated `{` or non-JSON text).
|
|
**Expected**: HTTP 400 + RFC 7807 `ProblemDetails`. Post-AZ-809 (cycle 8) the failure surfaces via `GlobalExceptionHandler`'s `JsonException` branch (System.Text.Json `JsonReaderException` → `BadHttpRequestException` → 400). No stack trace leaks; correlationId present per AZ-353.
|
|
**Pass criterion**: HTTP 400; `Content-Type: application/problem+json`; body matches `error-shape.md` v1.0.0; no internal exception type or stack frame in `detail`.
|
|
|
|
---
|
|
|
|
## 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.
|
|
|