Files
satellite-provider/_docs/05_security/security_report_cycle8.md
T
Oleksandr Bezdieniezhnykh ac40a8b352 [AZ-808] [AZ-809] [AZ-810] [AZ-811] [AZ-812] Cycle 8 security audit
PASS_WITH_WARNINGS. Zero Critical / High.

New cycle-8 findings:
- F-AZ809-1 (Medium / A04 Insecure Design): unbounded
  geofences.polygons enables an authenticated DoS on
  POST /api/satellite/route. Cap candidate: 50 or 500.
- F-AZ810-1 (Low / A09): JsonException.Message echoed in
  UavUploadValidationFilter (new instance of cycle-7 F-AZ795-1
  pattern in a second code path).
- F-AZ810-2 (Low / Informational): UavTileMetadata.CapturedAt
  typed DateTime not DateTimeOffset; freshness window drifts in
  non-UTC dev environments. Zero impact in UTC-deployed prod.

Carry-overs (cycle 7): F-AZ795-1, F-AZ795-2, D-AZ795-1 still
open. Cycle 4 D2-cy4 still open (test-runtime Medium).

Cycle-8 architectural wins recorded: per-endpoint validation
reached 100% coverage; three approved validation paths
formalised; OSM wire-format normalisation under strict mode
(AZ-812); UAV-handler defence-in-depth retained.

Highest-priority cycle-9 follow-up: F-AZ809-1 polygon cap.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 15:17:31 +03:00

16 KiB

Security Audit Report (Cycle 8)

Date: 2026-05-23 Scope: Cycle-8 delta over the cycle-7 audit (_docs/05_security/security_report_cycle7.md). Cycle-8 surface = AZ-808 (region POST validator) + AZ-809 (route POST validator + per-point + per-polygon) + AZ-810 (UAV upload metadata validator + custom UavUploadValidationFilter) + AZ-811 (lat/lon GET validator + RejectUnknownQueryParamsEndpointFilter) + AZ-812 (region-API Latitude/LongitudeLat/Lon wire rename — OSM convention). Trigger: /autodev Step 14 (Security Audit) — feature cycle 8, post-implementation, post-test-spec-sync, post-docs-update. Verdict (cycle-8 delta): PASS_WITH_WARNINGS — 1 Medium (F-AZ809-1) + 2 new Lows (F-AZ810-1, F-AZ810-2) + 2 cycle-7 Low carry-overs (F-AZ795-1, F-AZ795-2) + 1 cycle-7 dependency Low carry-over (D-AZ795-1) + 1 cycle-4 dependency Medium carry-over (D2-cy4). Zero Critical / High. Verdict (cumulative): PASS_WITH_WARNINGS — 1 cycle-4 Medium (D2-cy4, test-runtime only) + 1 cycle-8 Medium (F-AZ809-1, auth-gated DoS) + multiple Lows.

Summary

Severity Cycle 7 delta Cycle 8 delta Cumulative
Critical 0 0 0
High 0 0 0
Medium 0 1 NEW (F-AZ809-1 — unbounded geofences.polygons enables an authenticated DoS on POST /api/satellite/route) 2 (F-AZ809-1 cycle-8 + D2-cy4 cycle-4 carry — Microsoft.NET.Test.Sdk 17.8.0 transitive NuGet.Frameworks; test-runtime exposure only)
Low 3 (F-AZ795-1, F-AZ795-2, D-AZ795-1) 2 NEW (F-AZ810-1 — JsonException.Message echo in UavUploadValidationFilter; F-AZ810-2 — DateTime vs DateTimeOffset in UavTileMetadata.CapturedAt) 5+ (3 cycle-7 carry + 2 cycle-8 new)

OWASP Top 10:2021 Assessment

Category Status (cycle-8 delta) Findings
A01 — Broken Access Control PASS
A02 — Cryptographic Failures N/A No crypto in cycle 8
A03 — Injection PASS — (cycle 8 strengthens — strict deserialization now backed by per-endpoint range checks at all four newly-validated endpoints)
A04 — Insecure Design PASS_WITH_WARNINGS F-AZ809-1 (Medium — unbounded geofences.polygons cap absence)
A05 — Security Misconfiguration PASS (Informational note re: global Kestrel 500 MiB body limit; not a finding)
A06 — Vulnerable Components PASS_WITH_WARNINGS D-AZ795-1 (Low — cycle-7 carry; FluentValidation 12.0.0 → 12.1.1 hardening still available)
A07 — Auth Failures PASS — (JWT unchanged; every cycle-8 endpoint retains RequireAuthorization())
A08 — Data Integrity Failures N/A No CI/CD or artifact-signing surface in cycle 8
A09 — Logging Failures PASS_WITH_WARNINGS F-AZ810-1 (Low — new instance of F-AZ795-1 pattern in UavUploadValidationFilter) + F-AZ795-1 + F-AZ795-2 (Low cycle-7 carry-overs) + F-AZ810-2 (Low / Informational time-handling)
A10 — SSRF N/A No URL-input fields in cycle 8

Findings

# Severity Category Location Title
F-AZ809-1 Medium Insecure Design (A04) SatelliteProvider.Api/Validators/CreateRouteRequestValidator.cs:72-82 Unbounded geofences.polygons collection enables an authenticated DoS on POST /api/satellite/route
F-AZ810-1 Low Information Disclosure (A09) SatelliteProvider.Api/Validators/UavUploadValidationFilter.cs:75-84 JsonException.Message propagated to client in new code path (parallel to cycle-7 F-AZ795-1)
F-AZ810-2 Low Time-handling correctness (A09 adjacent) SatelliteProvider.Common/DTO/UavTileMetadata.cs:30 + SatelliteProvider.Api/Validators/UavTileMetadataValidator.cs:52-60 UavTileMetadata.CapturedAt typed DateTime not DateTimeOffset — freshness window drifts by host TZ in non-UTC dev environments
F-AZ795-1 Low Information Disclosure (A09) SatelliteProvider.Api/GlobalExceptionHandler.cs:108-117 (cycle-7 carry) JsonException.Message propagated to client in 400 response — still open
F-AZ795-2 Low Information Disclosure (A09) SatelliteProvider.Api/GlobalExceptionHandler.cs:88-93 (cycle-7 carry) Generic BadHttpRequestException.Message propagated as Detail for non-JSON 400 paths — still open
D-AZ795-1 Low Vulnerable & Outdated Components (A06) NuGet (cycle-7 carry) FluentValidation + FluentValidation.DependencyInjectionExtensions 12.0.0 → 12.1.1 (hardening release; no published CVE) — still open
D2-cy4 Medium Vulnerable & Outdated Components (A06) NuGet (test runtime) (cycle-4 carry) Microsoft.NET.Test.Sdk 17.8.0 transitive NuGet.Frameworks — test-runtime exposure only — still open

Finding Details

F-AZ809-1: Unbounded geofences.polygons collection enables an authenticated DoS (Medium / A04 — Insecure Design)

  • Location: SatelliteProvider.Api/Validators/CreateRouteRequestValidator.cs:72-82
  • Description: The cycle-8 CreateRouteRequestValidator chains RuleForEach(req => req.Geofences!.Polygons).SetValidator(new GeofencePolygonValidator()) but enforces only NotEmpty on the collection — no upper bound on Geofences.Polygons.Count. The sibling Points collection IS capped at 500; the global KestrelServerOptions.Limits.MaxRequestBodySize is set to 500 MiB to accommodate the UAV upload endpoint and applies to every route. With ~90 bytes per minimum-shape polygon JSON, an authenticated caller can submit ~5.8 million polygons in a single request; each invalid polygon yields ~3 ValidationFailure allocations, for ~17 million ValidationFailure objects in worst case — sufficient to saturate the LOH and trigger a full GC pass.
  • Impact: Medium. Auth-gated (RequireAuthorization() at Program.cs:267) — only tenant operators with a valid JWT can reach the endpoint. Within the cycle-8 threat model this is contained; promotion risk if the route endpoint is later exposed to an untrusted-tenant audience.
  • Remediation: Add Must(p => p is null || p.Count <= MaxPolygons) with a defensible upper bound. Reasonable cap candidates: 50 (consistent with the historical use case — geofence rectangles for AOI restriction); 500 (consistent with the sibling Points cap on the same DTO). The pattern across the API is "cap every collection field" — one collection slipped past in cycle 8.
  • Status: filed for cycle 9 as the highest-priority follow-up under AZ-809 (or a sibling child ticket).

F-AZ810-1: JsonException.Message propagated to client in UavUploadValidationFilter (Low / A09 — Information Disclosure)

  • Location: SatelliteProvider.Api/Validators/UavUploadValidationFilter.cs:75-84 (the catch (JsonException ex) block of the metadata parse)
  • Description: The cycle-8 UavUploadValidationFilter echoes the raw JsonException.Message directly to the client as the value of errors["metadata"] — the SAME information-disclosure pattern as cycle-7 F-AZ795-1 (in GlobalExceptionHandler.cs), introduced in a second code path that bypasses the global exception handler (the filter intercepts and returns Results.ValidationProblem(...) directly).
  • Impact: Low. Auth-gated (.RequireAuthorization(SatellitePermissions.UavUploadPolicy) at Program.cs:238) — only callers holding a valid JWT with the GPS permission claim can reach the filter. Leaks System.* type names + JSON parse positions — content already inferable from the OpenAPI spec.
  • Remediation: Sanitise the response message to a generic string (e.g. "metadata could not be parsed as JSON. See the server log for details.") while continuing to log the raw ex.Message server-side under the request's correlationId. Lock-step with F-AZ795-1's remediation. Add an integration-test assertion in UavUploadValidationTests that no System.* substring appears in the response body's errors[] value.
  • Status: filed for cycle 9 as a child of the same F-AZ795-1 ticket — both call sites get the sanitiser at once.

F-AZ810-2: UavTileMetadata.CapturedAt typed DateTime not DateTimeOffset (Low / Informational — Time-handling correctness)

  • Location: SatelliteProvider.Common/DTO/UavTileMetadata.cs:30 + SatelliteProvider.Api/Validators/UavTileMetadataValidator.cs:52-60
  • Description: When System.Text.Json deserializes an ISO-8601 string without a Z / +HH:MM suffix into a DateTime, Kind = Unspecified. DateTime.ToUniversalTime() treats Unspecified values as local time — the freshness comparison drifts by the host's timezone offset. In a UTC-deployed prod container the local offset is zero; in a developer's local environment with TZ=Europe/Kyiv (UTC+02:00 or +03:00) a capturedAt value of "2026-05-22T12:00:00" would be treated as 2026-05-22T10:00:00Z in summer, shifting the freshness window by the offset.
  • Impact: Low / Informational. Zero observable impact in the supported deployment configuration (Docker containers run in UTC; the cycle-2 docker-compose.yml does not override TZ). No security exploit: an attacker cannot force the server's TZ. Reported as a defence-in-depth correctness item.
  • Remediation (two options): (1) Change the DTO type to DateTimeOffset + custom JSON converter rejecting offset-less ISO-8601. (2) Add a FluentValidation rule rejecting DateTime.Kind == DateTimeKind.Unspecified. Option 2 is the minimum behaviour-preserving fix; Option 1 is correct for v2.0 of uav-tile-upload.md.
  • Status: filed for cycle 9 as Low; not release-blocking — every documented client and every integration-test fixture sends the Z suffix.

F-AZ795-1 / F-AZ795-2 / D-AZ795-1: see _docs/05_security/security_report_cycle7.md § "Finding Details" — all three carry-overs are unchanged at cycle-8 tip. The cycle-8 F-AZ810-1 finding is a NEW instance of the F-AZ795-1 pattern in a different code path; both surfaces must be sanitised in lock-step.

D2-cy4: see _docs/05_security/dependency_scan_cycle4.md — unchanged at cycle-8 tip. Test-runtime exposure only.

Dependency Vulnerabilities

Package CVE Severity Fix Version Status
FluentValidation 12.0.0 — (hardening only) Low 12.1.1 D-AZ795-1 cycle-7 carry — still open
FluentValidation.DependencyInjectionExtensions 12.0.0 — (hardening only) Low 12.1.1 D-AZ795-1 cycle-7 carry — still open (same item)
Microsoft.NET.Test.Sdk 17.8.0 (transitive NuGet.Frameworks) — (cycle-4 carry) Medium TBD (next Test SDK refresh cycle) D2-cy4 cycle-4 carry — still open

No new dependency vulnerabilities in cycle 8. Cycle 8 added zero new packages.

Recommendations

Immediate (Critical/High)

None.

Short-term (Medium)

  1. Add MaxPolygons cap to CreateRouteRequestValidator (F-AZ809-1) — single .Must(...) chain on geofences.polygons. Recommended MaxPolygons = 50 (use-case-driven) or 500 (sibling-collection-consistent — matches Points cap on the same DTO). One-line code change + matching unit test. Highest-priority cycle-9 follow-up.

Long-term (Low / Hardening)

  1. Sanitise client-visible 400 messages (F-AZ795-1 + F-AZ795-2 + F-AZ810-1) — single sanitiser applied to BOTH GlobalExceptionHandler.WriteClientErrorAsync AND UavUploadValidationFilter.InvokeAsync. Pre-existing-class instance in UavTileUploadHandler.cs:80 must also be sanitised at the same time so the defence-in-depth path doesn't continue leaking. Add matching test assertions in TileInventoryValidationTests + UavUploadValidationTests. Estimated 2 hours. File as a small follow-up child of AZ-795 (epic).
  2. Bump FluentValidation 12.0.0 → 12.1.1 (D-AZ795-1) — single .csproj edit + regression test pass. No API surface change in 12.0.0 → 12.1.1 per the upstream changelog.
  3. Add DateTimeKind.Unspecified rejection rule to UavTileMetadataValidator (F-AZ810-2 Option 2) — single .Must(capturedAt => capturedAt.Kind != DateTimeKind.Unspecified) rule. Doesn't break any documented client (all examples send Z suffix). One-line code change + matching unit test.
  4. Per-endpoint Kestrel body-size narrowing (informational hardening) — apply .WithMetadata(new RequestSizeLimitMetadata { MaxRequestBodySize = … }) to each non-upload endpoint so the 500 MiB global is constrained for JSON endpoints (e.g. 1 MiB for region/route, 10 MiB for inventory). Reduces F-AZ809-1's exploit surface even after the polygon cap is applied. Tracker child of AZ-795 or a standalone hardening task.

Pre-existing-class follow-up (Low / Informational)

  1. Drop the try/catch (ArgumentException) block in the CreateRoute handler (Program.cs:387-399) — pre-cycle-8 inconsistency that cycle 8 reduced the reachability of but did not eliminate. The validator now intercepts most ArgumentException cases; the catch leaks ex.Message in a non-conformant {error: string} shape. Fold into the same cycle-9 follow-up as #2 above.

Cumulative reminders (carry-overs)

  • D2-cy4Microsoft.NET.Test.Sdk 17.8.0 transitive NuGet.Frameworks Medium, test-runtime only. Owned by the next Test SDK refresh.

Cycle-8 Architectural Wins

The audit specifically wants to record four improvements introduced this cycle:

  1. Per-endpoint input-validation completion — the AZ-795 strict-validation epic introduced in cycle 7 reached 100% coverage in cycle 8. Every public-facing input endpoint (region POST, route POST, lat/lon GET, inventory POST, UAV upload) now runs a validator behind the same error-shape.md v1.0.0 contract. Future drift visibility is high; the architecture doc's coverage table is now complete.
  2. Three approved validation paths formalised — the architecture doc (_docs/02_document/architecture.md § 9) now codifies three approved validation paths: WithValidation<T>() for JSON-body endpoints; WithValidation<T>() + RejectUnknownQueryParamsEndpointFilter for query-string endpoints; custom IEndpointFilter for non-standard wire formats. Future endpoints have a clear pattern to follow; an audit can verify any new endpoint via the chosen-path criterion.
  3. Wire-format normalisation under strict mode (AZ-812) — Latitude / LongitudeLat / Lon on the region endpoint aligns with OSM convention used everywhere else in the API. Under UnmappedMemberHandling.Disallow (cycle 7 global), legacy Latitude / Longitude payloads are now rejected at the deserializer with a structured 400 — no silent coercion, no compatibility-shim layer. This is a hard breaking change for any consumer still on the old shape, but it is intentional and documented as the post-cycle-8 contract.
  4. Defence-in-depth retained for the UAV upload handlerUavTileUploadHandler.HandleAsync retains every envelope check it had pre-cycle-8 (metadata presence, JSON parse, Items.Count == 0, Items.Count != files.Count, Items.Count > MaxBatchSize). The new UavUploadValidationFilter is additive — it intercepts the primary endpoint path but does not weaken the service-layer guard for direct callers (e.g. unit tests). The two layers' shape choices are mutually compatible.

Verdict

PASS_WITH_WARNINGS — 1 Medium (F-AZ809-1, auth-gated DoS on the route endpoint via unbounded geofences.polygons) + 2 cycle-8 Lows + 3 cycle-7 Low carry-overs + 1 cycle-4 Medium carry-over (test-runtime only). Zero Critical / High.

Per the skill's verdict-logic, Medium severity yields PASS_WITH_WARNINGS. Cycle 8 is safe to release within its documented threat model (authenticated callers only on every endpoint). F-AZ809-1 must be the highest-priority cycle-9 follow-up because the missing collection cap is the only inconsistency across the API's otherwise-uniform "cap every collection field" pattern, and the global Kestrel body limit means the validator is the sole gate against an unbounded submission today.

Cumulative posture: PASS_WITH_WARNINGS (1 cycle-4 Medium carry-over + 1 cycle-8 Medium + multiple Lows). No regression of the cycle-7 PASS_WITH_WARNINGS posture; cycle 8 added one new Medium but completed an architecturally important hardening epic.