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>
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/Longitude → Lat/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
CreateRouteRequestValidatorchainsRuleForEach(req => req.Geofences!.Polygons).SetValidator(new GeofencePolygonValidator())but enforces onlyNotEmptyon the collection — no upper bound onGeofences.Polygons.Count. The siblingPointscollection IS capped at 500; the globalKestrelServerOptions.Limits.MaxRequestBodySizeis 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 ~3ValidationFailureallocations, for ~17 millionValidationFailureobjects in worst case — sufficient to saturate the LOH and trigger a full GC pass. - Impact: Medium. Auth-gated (
RequireAuthorization()atProgram.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 siblingPointscap 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(thecatch (JsonException ex)block of the metadata parse) - Description: The cycle-8
UavUploadValidationFilterechoes the rawJsonException.Messagedirectly to the client as the value oferrors["metadata"]— the SAME information-disclosure pattern as cycle-7 F-AZ795-1 (inGlobalExceptionHandler.cs), introduced in a second code path that bypasses the global exception handler (the filter intercepts and returnsResults.ValidationProblem(...)directly). - Impact: Low. Auth-gated (
.RequireAuthorization(SatellitePermissions.UavUploadPolicy)atProgram.cs:238) — only callers holding a valid JWT with theGPSpermission claim can reach the filter. LeaksSystem.*type names + JSON parse positions — content already inferable from the OpenAPI spec. - Remediation: Sanitise the response message to a generic string (e.g.
"metadatacould not be parsed as JSON. See the server log for details.") while continuing to log the rawex.Messageserver-side under the request'scorrelationId. Lock-step with F-AZ795-1's remediation. Add an integration-test assertion inUavUploadValidationTeststhat noSystem.*substring appears in the response body'serrors[]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.Jsondeserializes an ISO-8601 string without aZ/+HH:MMsuffix into aDateTime,Kind = Unspecified.DateTime.ToUniversalTime()treatsUnspecifiedvalues 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 withTZ=Europe/Kyiv(UTC+02:00 or +03:00) acapturedAtvalue of"2026-05-22T12:00:00"would be treated as2026-05-22T10:00:00Zin 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.ymldoes not overrideTZ). 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 rejectingDateTime.Kind == DateTimeKind.Unspecified. Option 2 is the minimum behaviour-preserving fix; Option 1 is correct for v2.0 ofuav-tile-upload.md. - Status: filed for cycle 9 as Low; not release-blocking — every documented client and every integration-test fixture sends the
Zsuffix.
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)
- Add
MaxPolygonscap toCreateRouteRequestValidator(F-AZ809-1) — single.Must(...)chain ongeofences.polygons. RecommendedMaxPolygons = 50(use-case-driven) or500(sibling-collection-consistent — matchesPointscap on the same DTO). One-line code change + matching unit test. Highest-priority cycle-9 follow-up.
Long-term (Low / Hardening)
- Sanitise client-visible 400 messages (F-AZ795-1 + F-AZ795-2 + F-AZ810-1) — single sanitiser applied to BOTH
GlobalExceptionHandler.WriteClientErrorAsyncANDUavUploadValidationFilter.InvokeAsync. Pre-existing-class instance inUavTileUploadHandler.cs:80must also be sanitised at the same time so the defence-in-depth path doesn't continue leaking. Add matching test assertions inTileInventoryValidationTests+UavUploadValidationTests. Estimated 2 hours. File as a small follow-up child of AZ-795 (epic). - Bump FluentValidation 12.0.0 → 12.1.1 (D-AZ795-1) — single
.csprojedit + regression test pass. No API surface change in 12.0.0 → 12.1.1 per the upstream changelog. - Add
DateTimeKind.Unspecifiedrejection rule toUavTileMetadataValidator(F-AZ810-2 Option 2) — single.Must(capturedAt => capturedAt.Kind != DateTimeKind.Unspecified)rule. Doesn't break any documented client (all examples sendZsuffix). One-line code change + matching unit test. - 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)
- Drop the
try/catch (ArgumentException)block in theCreateRoutehandler (Program.cs:387-399) — pre-cycle-8 inconsistency that cycle 8 reduced the reachability of but did not eliminate. The validator now intercepts mostArgumentExceptioncases; the catch leaksex.Messagein a non-conformant{error: string}shape. Fold into the same cycle-9 follow-up as #2 above.
Cumulative reminders (carry-overs)
- D2-cy4 —
Microsoft.NET.Test.Sdk 17.8.0transitiveNuGet.FrameworksMedium, 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:
- 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.mdv1.0.0 contract. Future drift visibility is high; the architecture doc's coverage table is now complete. - 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>()+RejectUnknownQueryParamsEndpointFilterfor query-string endpoints; customIEndpointFilterfor non-standard wire formats. Future endpoints have a clear pattern to follow; an audit can verify any new endpoint via the chosen-path criterion. - Wire-format normalisation under strict mode (AZ-812) —
Latitude/Longitude→Lat/Lonon the region endpoint aligns with OSM convention used everywhere else in the API. UnderUnmappedMemberHandling.Disallow(cycle 7 global), legacyLatitude/Longitudepayloads 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. - Defence-in-depth retained for the UAV upload handler —
UavTileUploadHandler.HandleAsyncretains every envelope check it had pre-cycle-8 (metadatapresence, JSON parse,Items.Count == 0,Items.Count != files.Count,Items.Count > MaxBatchSize). The newUavUploadValidationFilteris 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.