Step 12 (Test-Spec Sync): adds BT-27 for the AZ-796 9-rule validation surface and 12 cycle-7 AC rows + Coverage Summary update to traceability-matrix.md. Step 13 (Update Docs): module-layout + module docs for the new SatelliteProvider.Api/Validators namespace + GlobalExceptionHandler + updated TileInventory DTO; tests_unit + tests_integration document the new InventoryRequestValidatorTests (16 unit tests covering all 9 rules) + TileInventoryValidationTests (16 integration tests) + ProblemDetailsAssertions support; glossary entries for Validation Problem Details / FluentValidation / Unmapped Member Handling; system-flows F8 (Tile Inventory Bulk Lookup) expanded with deserializer + validator gates and a 13-row Validation Surface table; data_parameters § Tile Inventory documents the v2 input schema + constraints; ripple_log_cycle7 captures the doc-side ripple decisions. Step 14 (Security Audit): 5-phase audit ran; verdict PASS_WITH_WARNINGS (3 Low findings — D-AZ795-1 FluentValidation 12.0.0 -> 12.1.1 recommended bump, F-AZ795-1 JsonException.Message leak in 400 detail, F-AZ795-2 BadHttpRequestException.Message leak). No Critical / High; auth runs before validation (confirmed in Program.cs); two NuGet additions (FluentValidation 12.0.0 + .DependencyInjectionExtensions 12.0.0) both CVE-clean. Per-phase reports plus consolidated security_report_cycle7.md. Step 15 (Performance Test): docker compose stack used for perf run, scripts/run-performance-tests.sh exited 0 with 8/8 scenarios PASS (second consecutive clean exit-0); added PT-09 cycle-7 smoke probe (v2 z/x/y schema, 2500-tile all-miss batch) measuring min=27ms median=44ms p95=73ms max=86ms (13.7x under AZ-505 AC-4 1000ms budget). PT-07/08 improvements traced to the cycle-6 TLS handshake-overhead identification, not application-side change. Co-authored-by: Cursor <cursoragent@cursor.com>
9.1 KiB
Security Audit Report (Cycle 7)
Date: 2026-05-22
Scope: Cycle-7 delta over the cycle-5 audit (_docs/05_security/security_report_cycle5.md); cycle 6 produced no security report, so cycle 5 is the last full baseline. Cycle-7 surface = AZ-794 (tileZoom/tileX/tileY → z/x/y rename) + AZ-795 (strict-validation epic: FluentValidation, UnmappedMemberHandling.Disallow, GlobalExceptionHandler, error-shape contract) + AZ-796 (inventory-endpoint 9-rule validator).
Trigger: /autodev Step 14 (Security Audit) — feature cycle 7, post-implementation, post-test-spec-sync, post-docs-update.
Verdict (cycle-7 delta): PASS_WITH_WARNINGS (3 Low findings; no Critical/High/Medium).
Verdict (cumulative): PASS_WITH_WARNINGS (carries forward 1 cycle-4 Medium dep finding via D2-cy4 + 2 cycle-5 Low informational notes + cycle-7's 3 Lows).
Summary
| Severity | Cycle 5 delta | Cycle 7 delta | Cumulative |
|---|---|---|---|
| Critical | 0 | 0 | 0 |
| High | 0 | 0 | 0 |
| Medium | 0 | 0 | 1 (D2-cy4 carry — Microsoft.NET.Test.Sdk 17.8.0 transitive NuGet.Frameworks; test-runtime exposure only) |
| Low | 2 informational notes | 3 NEW (F-AZ795-1, F-AZ795-2, D-AZ795-1) | 5+ |
OWASP Top 10:2021 Assessment
| Category | Status (cycle-7 delta) | Findings |
|---|---|---|
| A01 — Broken Access Control | PASS | — |
| A02 — Cryptographic Failures | N/A | No crypto in cycle 7 |
| A03 — Injection | PASS | — |
| A04 — Insecure Design | PASS (improvement) | AZ-795 / AZ-796 centralise validation behind one filter + one error handler — direct improvement |
| A05 — Security Misconfiguration | PASS | UnmappedMemberHandling.Disallow is defense-in-depth (mass-assignment prevention) |
| A06 — Vulnerable Components | PASS_WITH_WARNINGS | D-AZ795-1 (Low; bump 12.0.0 → 12.1.1 hardening release) |
| A07 — Auth Failures | PASS | JWT validation unchanged; new endpoint filter cannot run for anonymous callers |
| A08 — Data Integrity Failures | N/A | No CI/CD or artifact-signing surface in cycle 7 |
| A09 — Logging Failures | PASS_WITH_WARNINGS | F-AZ795-1 + F-AZ795-2 (Lows; JsonException.Message / BadHttpRequestException.Message echoed to client) |
| A10 — SSRF | N/A | No URL-input fields in cycle 7 |
Findings
| # | Severity | Category | Location | Title |
|---|---|---|---|---|
| F-AZ795-1 | Low | Information Disclosure (A09) | SatelliteProvider.Api/GlobalExceptionHandler.cs:108–117 |
JsonException.Message propagated to client in 400 response (type-name + parse-position leak) |
| F-AZ795-2 | Low | Information Disclosure (A09) | SatelliteProvider.Api/GlobalExceptionHandler.cs:88–93 |
Generic BadHttpRequestException.Message propagated as Detail for non-JSON 400 paths |
| D-AZ795-1 | Low | Vulnerable & Outdated Components (A06) | NuGet | FluentValidation + FluentValidation.DependencyInjectionExtensions 12.0.0 → 12.1.1 (hardening release; no published CVE) |
Finding Details
F-AZ795-1: JsonException.Message propagated to client in 400 response (Low / A09 — Information Disclosure)
- Location:
SatelliteProvider.Api/GlobalExceptionHandler.cs:108–117(TryExtractDeserializationErrors) - Description:
System.Text.Json.JsonException.Messageis echoed in the 400ValidationProblemDetails.errors[fieldPath]array. The default message includes the offending .NET type (System.Int32,System.Guid, …), the JSON path (already separately captured as the key), and the byte position / line number in the payload — e.g. "The JSON value could not be converted to System.Int32. Path: $.tiles[0].z | LineNumber: 0 | BytePositionInLine: 27.". - Impact: Low. The
UseAuthentication+UseAuthorizationmiddleware short-circuits anonymous callers with 401 before any endpoint filter runs, so the leak is only reachable by authenticated callers. The leaked content (type names, parse positions,System.Text.Jsonfingerprint) is already inferable from the OpenAPI spec at/swagger/v1/swagger.json; this finding narrows the attack surface for an authenticated tenant operator but does not expose secrets, PII, or pivot vectors. - Remediation: Sanitise the response message to a generic string (e.g.
"Could not deserialize value at this field path.") while continuing to log the rawjsonEx.Messageserver-side under the request'scorrelationId. Updateerror-shape.mdtest casevalidation-type-mismatchand the integration tests to assert noSystem.*substring appears in anyerrors[]value. - Status: filed for next cycle.
F-AZ795-2: Generic BadHttpRequestException.Message propagated as Detail (Low / A09 — Information Disclosure)
- Location:
SatelliteProvider.Api/GlobalExceptionHandler.cs:88–93(fallback 400 path when there is noJsonExceptioninner exception) - Description: When
BadHttpRequestExceptionhas noJsonExceptioninner exception (e.g. framework model-binding failures, unsupportedContent-Type, oversized request bodies), the framework-providedMessageis echoed back asProblemDetails.Detail. ASP.NET Core message strings for these paths can include parameter names and (rarely) framework version hints. - Impact: Same severity as F-AZ795-1. Pre-existing-class issue (model-binding messages were always shaped this way under ASP.NET Core); cycle 7 didn't introduce or worsen it.
- Remediation: Same as F-AZ795-1 — sanitise the
Detailto a generic string and log the rawMessageserver-side. Best done in tandem with F-AZ795-1. - Status: filed for next cycle.
D-AZ795-1: FluentValidation 12.0.0 → 12.1.1 hardening refresh (Low / A06 — Vulnerable & Outdated Components)
- Location:
SatelliteProvider.Api/SatelliteProvider.Api.csproj(FluentValidation+FluentValidation.DependencyInjectionExtensions) - Description: 12.0.0 has no known CVEs (verified against GitHub Security Advisories, NVD, ReversingLabs Spectra Assure). 12.1.1 is the latest version (~5 months newer at audit time) and is a hardening release — minor upstream fixes, no security advisories.
- Impact: Low. Pure forward-compatibility hardening.
- Remediation: Bump both packages to 12.1.1 in a future minor-dependency-roll cycle.
- Status: filed for next cycle. Not release-blocking.
Dependency Vulnerabilities
| Package | CVE | Severity | Fix Version | Status |
|---|---|---|---|---|
| FluentValidation 12.0.0 | — (hardening only) | Low | 12.1.1 | D-AZ795-1 — filed |
| FluentValidation.DependencyInjectionExtensions 12.0.0 | — (hardening only) | Low | 12.1.1 | D-AZ795-1 — filed (same item) |
Microsoft.NET.Test.Sdk 17.8.0 (transitive NuGet.Frameworks) |
— (cycle-4 carry-over D2-cy4) | Medium | TBD (next Test SDK refresh cycle) | carry-over from cycle 4 — owned by a separate unscheduled task |
Recommendations
Immediate (Critical/High)
None.
Short-term (Medium)
None new in cycle 7. Cycle-4 carry-over D2-cy4 (Microsoft.NET.Test.Sdk Medium) remains in the backlog.
Long-term (Low / Hardening)
- Sanitise client-visible 400 messages (F-AZ795-1 + F-AZ795-2). Single change in
GlobalExceptionHandler.WriteClientErrorAsync+ matching test assertion. Estimated 1 hour of effort. Should be filed as a small follow-up child of AZ-795 (or as a standalone task under the same epic). - Bump FluentValidation 12.0.0 → 12.1.1 (D-AZ795-1). Single
.csprojedit + a regression test pass; no API surface change in 12.0.0 → 12.1.1 per the upstream changelog.
Cumulative reminders (carry-overs)
- Cycle-4 D2-cy4 —
Microsoft.NET.Test.Sdk 17.8.0transitiveNuGet.FrameworksMedium-severity finding, test-runtime exposure only. Owned by the next Test SDK refresh.
Cycle-7 Architectural Wins
The audit specifically wants to record three improvements introduced this cycle:
- Mass-assignment prevention by default —
UnmappedMemberHandling.Disallowon the global JSON pipeline rejects any unknown root or nested field across every public endpoint. The cycle-7 acceptance criteria explicitly enumerate this for the inventory endpoint; the protection is in force for every other endpoint that consumes a JSON body too. - Uniform 4xx contract —
error-shape.mdv1.0.0 unifies the wire shape across two failure layers (deserializer + FluentValidation). Future child tickets reuseValidationEndpointFilter<T>,ProblemDetailsAssertions, and the contract without adding new infrastructure. This dramatically reduces the chance of future endpoints drifting into their own bespoke error shapes. - Auth-before-validation invariant verified — endpoint filters added via
WithValidation<T>()cannot run for unauthenticated callers (the routing pipeline runsUseAuthorizationBEFORE the endpoint filter chain). The audit explicitly verified the cycle-7 inventory endpoint and re-asserted the invariant in this report.
Verdict
PASS_WITH_WARNINGS — 3 Lows, 0 Mediums (cycle-7 delta), 0 Highs, 0 Criticals. Cycle 7 is safe to release. The 3 Lows are filed for follow-up cycles and do not block release.
Cumulative posture: PASS_WITH_WARNINGS (1 cycle-4 Medium carry-over via D2-cy4 + Lows above). No regression of the cycle-5 PASS posture.