# 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.Message` is echoed in the 400 `ValidationProblemDetails.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` + `UseAuthorization` middleware 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.Json` fingerprint) 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 raw `jsonEx.Message` server-side under the request's `correlationId`. Update `error-shape.md` test case `validation-type-mismatch` and the integration tests to assert no `System.*` substring appears in any `errors[]` 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 no `JsonException` inner exception)
- Description: When `BadHttpRequestException` has no `JsonException` inner exception (e.g. framework model-binding failures, unsupported `Content-Type`, oversized request bodies), the framework-provided `Message` is echoed back as `ProblemDetails.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 `Detail` to a generic string and log the raw `Message` server-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)

1. **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).
2. **Bump FluentValidation 12.0.0 → 12.1.1** (D-AZ795-1). Single `.csproj` edit + 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.0` transitive `NuGet.Frameworks` Medium-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:

1. **Mass-assignment prevention by default** — `UnmappedMemberHandling.Disallow` on 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.
2. **Uniform 4xx contract** — `error-shape.md` v1.0.0 unifies the wire shape across two failure layers (deserializer + FluentValidation). Future child tickets reuse `ValidationEndpointFilter<T>`, `ProblemDetailsAssertions`, and the contract without adding new infrastructure. This dramatically reduces the chance of future endpoints drifting into their own bespoke error shapes.
3. **Auth-before-validation invariant verified** — endpoint filters added via `WithValidation<T>()` cannot run for unauthenticated callers (the routing pipeline runs `UseAuthorization` BEFORE 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.
