Files
Oleksandr Bezdieniezhnykh 8fca6e0209 [AZ-809] F-AZ809-1: cap geofences.polygons at 50 (security audit)
Closes the cycle-8 Medium DoS finding. Without the cap, an
authenticated caller could submit millions of bbox polygons in a
single 500 MiB request (Kestrel global limit) and saturate the
FluentValidation allocator on the validator hot path; each polygon
is ~90 bytes of JSON, so the body limit is not a useful gate.

Realistic use is 1-10 polygons per route — 50 leaves 5x headroom
while bounding the worst-case allocation.

Layers:
- CreateRouteRequestValidator: MaxPolygons = 50 + Must(...) chained
  before RuleForEach so the count error fires at "geofences.polygons"
  (not the leaf path).
- Unit: Validate_GeofencePolygonsTooMany_FailsCountRule.
- Integration: GeofencePolygonsTooMany_Returns400 (51 valid bbox
  polygons -> HTTP 400 + errors["geofences.polygons"]).
- Contract: route-creation.md -> v1.0.1 patch (tightening an
  existing range). New Inv-10, new geofence-polygons-too-many
  test case, changelog row.
- Test spec: BT-29 sub-case 9b + AZ-809 AC-1b row in the
  traceability matrix.
- Security report: F-AZ809-1 marked RESOLVED in cycle 8; verdict
  remains PASS_WITH_WARNINGS (Lows + carry-overs unchanged).

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

112 lines
12 KiB
Markdown

# OWASP Top 10 Review (Cycle 8)
**Date**: 2026-05-23
**Mode**: Delta scan against OWASP Top 10:2021 (current at audit time per https://owasp.org/www-project-top-ten/ — verified 2026-05-23; the 2025 candidate revision is still in public-comment phase and not adopted).
**Scope**: Cycle-8 delta only — AZ-808 (region POST validator), AZ-809 (route POST validator + per-point + per-polygon), AZ-810 (UAV upload metadata validator + custom filter), AZ-811 (lat/lon GET validator + unknown-query-param filter), AZ-812 (region-API `Latitude`/`Longitude``Lat`/`Lon` rename). Earlier cycles' OWASP reviews remain authoritative for their respective surfaces; this file does NOT re-walk the cycle-5 / cycle-7 baselines.
## A01 — Broken Access Control
**Status**: PASS
- `.RequireAuthorization()` is preserved on every cycle-8 endpoint:
- `POST /api/satellite/request` at `Program.cs:251`
- `POST /api/satellite/route` at `Program.cs:267`
- `GET /api/satellite/tiles/latlon` at `Program.cs:213`
- `POST /api/satellite/upload` at `Program.cs:238` (additionally requires the `GPS` permission claim via `SatellitePermissions.UavUploadPolicy`).
- Endpoint-filter execution order is governed by ASP.NET Core's middleware → routing → endpoint-filter pipeline. `app.UseAuthorization()` (line 206) reads the endpoint metadata produced by `.RequireAuthorization()` and short-circuits anonymous callers with 401 BEFORE the endpoint dispatch reaches any endpoint filter. Cycle-8 verification: none of the four new validation paths runs unless the caller is authenticated.
- The new `RejectUnknownQueryParamsEndpointFilter` (lat/lon GET) does not establish its own auth gate — it relies on the endpoint chain's existing `.RequireAuthorization()`. Anonymous query-param probing is impossible.
- No new CORS policy in cycle 8. `TilesCors` (cycle-6 baseline) is unchanged.
- No new IDOR paths — the four endpoints operate on caller-supplied identifiers but do not couple them to any tenant or owner field; tiles remain globally-scoped in the post-AZ-484 model. Geospatial identifiers (`Lat`, `Lon`, `Z/X/Y`) are deterministic projections of physical reality, not capability tokens.
## A02 — Cryptographic Failures
**Status**: N/A (cycle 8)
- Cycle 8 has no cryptographic operations. JWT validation is unchanged from cycle 4 (`AddSatelliteJwt` — HS256 with ≥ 32-byte secret, `ValidateLifetime + ValidateIssuer + ValidateAudience = true`, ClockSkew = 30s).
- The cycle-5 UUIDv5 SHA-1 surface (`Uuidv5.Create`) is unaffected.
- TLS posture (Kestrel `Http1AndHttp2` with self-signed dev cert / ingress termination in prod) — unchanged from cycle 6.
- F-AZ810-2 (`DateTime` vs `DateTimeOffset` parsing) is NOT a crypto failure — it's a time-handling correctness concern documented under A09.
## A03 — Injection
**Status**: PASS
- No SQL / Dapper / Npgsql usage in any cycle-8 new file. (`grep -r 'Dapper\|Npgsql' SatelliteProvider.Api/Validators SatelliteProvider.Api/DTOs SatelliteProvider.Common/DTO` → zero matches across the cycle-8 surface.)
- No `Process.Start` / shell-out / `eval` in any cycle-8 new file.
- All inputs reaching the cycle-8 validators are strongly typed by the time the rules execute (`double`, `int`, `Guid`, `DateTime`, `string`, `IReadOnlyList<T>`). `System.Text.Json` has already parsed and rejected anything malformed before the validator runs, and the cycle-7 deserializer hardening (`UnmappedMemberHandling.Disallow`) is in force on every cycle-8 path including the multipart `metadata` field (`UavUploadValidationFilter.cs:73` uses the same global `JsonSerializerOptions` via `IOptions<JsonOptions>`).
- The cycle-8 wire-format rename (AZ-812) means the deserializer now strictly enforces `lat` / `lon` against the post-rename JSON schema — legacy `Latitude` / `Longitude` fields are rejected as unknown members, surfacing as `JsonException` → 400 via `GlobalExceptionHandler` (not silently bound to a fallback property). This is a small *anti-injection* improvement at the schema boundary.
## A04 — Insecure Design
**Status**: **PASS (post-follow-up)** — was PASS_WITH_WARNINGS at audit time; F-AZ809-1 was the only Medium and was resolved in the Step-14 follow-up commit (cycle 8). Post-follow-up posture is PASS.
- AZ-808 / AZ-809 / AZ-810 / AZ-811 are themselves a *design fix* for the remaining unprotected endpoints — completing the AZ-795 epic's per-endpoint rollout. Pre-cycle-8, four endpoints (region POST, route POST, lat/lon GET, UAV upload) used ad-hoc inline `try/catch` blocks or no input validation at all. Cycle 8 centralises every public endpoint behind one of three approved validation paths:
1. `WithValidation<T>()` for JSON-body endpoints (RegionRequest, CreateRouteRequest).
2. `WithValidation<T>()` + `RejectUnknownQueryParamsEndpointFilter` for query-string endpoints (GetTileByLatLonQuery).
3. Custom `IEndpointFilter` for non-standard wire formats (`UavUploadValidationFilter` for multipart).
- The cycle-7 architecture-doc § 9 coverage table now reaches **100% of public-facing input endpoints** with validators (region POST, route POST, lat/lon GET, inventory POST, UAV upload). Future drift visibility is high.
- **F-AZ809-1** (from `static_analysis_cycle8.md`) identified a design-level gap: `CreateRouteRequestValidator` lacked a max-count cap on `Geofences.Polygons`, in contrast to every other list-bearing field across the API (route `Points` ≤ 500, UAV `Items` ≤ 100, inventory `Tiles`/`LocationHashes` ≤ 5000). **Resolved in the Step-14 follow-up (cycle 8)**: `MaxPolygons = 50` cap added + matching unit + integration tests + `route-creation.md` v1.0.1 Inv-10. The pattern "cap every collection field" is now fully consistent across the API.
## A05 — Security Misconfiguration
**Status**: PASS
- `UnmappedMemberHandling.Disallow` (cycle-7 global default) is now backed by per-endpoint FluentValidation rules at every cycle-8 endpoint, completing the defence-in-depth hardening for mass-assignment prevention.
- Swagger exposure is still gated by `app.Environment.IsDevelopment()` (unchanged).
- `appsettings.Development.json` clearly tags DEV-ONLY JWT iss/aud values; `appsettings.json` ships empty so production fail-fast triggers if env vars are missing (unchanged from cycle 4).
- The new `AddTransient<UavUploadValidationFilter>` registration in `Program.cs:128` is correct — transient (not singleton) ensures each request gets a fresh filter instance, preventing accidental cross-request state retention through filter-instance fields.
- The cycle-7 `AddValidatorsFromAssemblyContaining<Program>()` scope rule is unchanged; the cycle-8 validators all live in `SatelliteProvider.Api.dll`, so the reflection scan correctly picks them up.
- **Note (informational, not a finding)**: the global Kestrel body-size limit (`MaxRequestBodySize = 500 MiB`) was originally set for the UAV upload endpoint in cycle 2 (AZ-488). It applies to every endpoint by default because Kestrel exposes a per-server, not per-endpoint, default. Cycle 8's F-AZ809-1 highlights the consequence: tight per-endpoint count caps in validators are now the primary defence on JSON endpoints, not the framework body limit. A future hardening cycle could narrow the body limit per-endpoint via `IRequestSizeLimitMetadata` on the `RouteHandlerBuilder` for non-upload endpoints — but this is hardening, not a finding.
## A06 — Vulnerable & Outdated Components
**Status**: PASS_WITH_WARNINGS (carry-over Low)
- See `dependency_scan_cycle8.md` for the full table. Summary:
- Cycle 8 added zero new packages and bumped zero existing packages.
- **D-AZ795-1** (cycle-7 carry-over): `FluentValidation` + `FluentValidation.DependencyInjectionExtensions` 12.0.0 → 12.1.1 hardening recommendation — still open. Cycle 8 did not bump.
- **D2-cy4** (cycle-4 carry-over): `Microsoft.NET.Test.Sdk 17.8.0` transitive `NuGet.Frameworks` Medium — test-runtime exposure only, still open.
## A07 — Identification and Authentication Failures
**Status**: PASS
- JWT validation parameters unchanged from cycle 4 (`AddSatelliteJwt`).
- No new auth-bypass paths introduced by cycle 8. The four new validators / filters cannot run for anonymous callers (see A01).
- The five new integration test files (`CreateRouteValidationTests`, `GetTileByLatLonValidationTests`, `RegionFieldRenameTests`, `RegionRequestValidationTests`, `UavUploadValidationTests`) all mint valid tokens via the shared `JwtTestHelpers.MintAuthenticated(...)` — proves the happy path is properly auth-gated and not relying on any test-only bypass.
- `RejectUnknownQueryParamsEndpointFilter` rejects malicious query keys (e.g. `?debug=1`, `?Authorization=…`) at the endpoint-filter layer — narrows the auth fingerprinting surface vs. the pre-cycle-8 behaviour where unknown keys silently bound to defaults.
## A08 — Software and Data Integrity Failures
**Status**: N/A (cycle 8)
- No CI/CD changes, no artifact-signing changes, no auto-update paths touched in cycle 8. The only CI/script-adjacent file modified is `scripts/run-performance-tests.sh` (wire-rename diff only — see `static_analysis_cycle8.md` § Test Code Review).
## A09 — Security Logging and Monitoring Failures
**Status**: PASS_WITH_WARNINGS (3 Lows — F-AZ810-1 new + F-AZ795-1 + F-AZ795-2 carry-over)
- `GlobalExceptionHandler` 5xx branch logs `Method`, `Path`, `correlationId`, and the exception object (via Serilog default). The 4xx branch does NOT log the exception (intentional, avoids noisy log signal from malformed-payload spam). Unchanged from cycle 7.
- The cycle-8 validators and the new `UavUploadValidationFilter` do NOT add any logging — they return `Results.ValidationProblem(...)` directly without any audit trail. This is consistent with cycle 7's `ValidationEndpointFilter<T>` posture (the response itself carries enough detail for the client to self-debug; the server logs would otherwise drown in 400s from typo-prone callers). Acceptable for the threat model.
- **F-AZ810-1** (NEW in cycle 8): `UavUploadValidationFilter.cs:82` echoes `JsonException.Message` to the client — same information-disclosure pattern as cycle-7 F-AZ795-1 in a NEW code path. Both surfaces leak `System.*` type names + parse positions. **Filed as Low**.
- **F-AZ795-1** (cycle-7 carry-over): `GlobalExceptionHandler.cs:108-117` — still open.
- **F-AZ795-2** (cycle-7 carry-over): `GlobalExceptionHandler.cs:88-93` — still open. Cycle 8 reduces practical reachability on the 4 newly-validated endpoints but doesn't eliminate it.
- **F-AZ810-2** (Informational, listed under A09 as a time-handling correctness concern with downstream logging/monitoring implications): `UavTileMetadata.CapturedAt` typed `DateTime` not `DateTimeOffset` — stale tiles could pass the freshness check in dev environments with non-UTC host TZ if the caller omits the `Z` suffix. Zero observable impact in UTC-deployed production. **Filed as Low / Informational**.
## A10 — Server-Side Request Forgery (SSRF)
**Status**: N/A (cycle 8)
- No URL-input fields, no outbound HTTP calls triggered by the cycle-8 surface. The pre-existing `GoogleMapsDownloaderV2` (outbound calls to Google Maps for tile fetches in the region/route processing paths) is not modified by cycle 8.
- The new `RejectUnknownQueryParamsEndpointFilter` works on the request's already-parsed query collection — no URL parsing, no outbound resolution.
## Cross-Reference with `security_approach.md`
The repo does not contain `_docs/00_problem/security_approach.md` (same as cycle 7). The OWASP review proceeds against the cycle-5 + cycle-6 + cycle-7 architectural decisions documented in `_docs/02_document/architecture.md` § 7 (Security Architecture) and § 9 (Strict wire-format validation at the API edge — added in cycle 7, extended in cycle 8). Cycle 8's input-validation completion cleanly extends those decisions; the F-AZ809-1 gap is the only deviation and is documented as such.
## Verdict (Phase 3)
**PASS_WITH_WARNINGS** (post-follow-up) — A04 (Insecure Design) is now **PASS** (F-AZ809-1 resolved in Step-14 follow-up); A06 + A09 remain `PASS_WITH_WARNINGS` from the carry-over Lows + the new F-AZ810-1 Low. Every other OWASP category is PASS or N/A.
Per the skill's verdict-logic, Medium severity yields PASS_WITH_WARNINGS — but cycle 8 resolved its only Medium in-cycle via the Step-14 follow-up commit before any production exposure. Post-follow-up posture is cleaner than the audit-time snapshot.