[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>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-23 15:29:10 +03:00
parent ac40a8b352
commit 8fca6e0209
9 changed files with 120 additions and 34 deletions
+4 -6
View File
@@ -32,7 +32,7 @@
## Findings
### F-AZ809-1 — Unbounded `geofences.polygons` collection enables an authenticated DoS via `CreateRouteRequest` (Medium / A04 — Insecure Design)
### F-AZ809-1 — Unbounded `geofences.polygons` collection enables an authenticated DoS via `CreateRouteRequest` (Medium / A04 — Insecure Design) — **RESOLVED in cycle 8 (Step-14 follow-up)**
- **Location**: `SatelliteProvider.Api/Validators/CreateRouteRequestValidator.cs:72-82` (`When(req => req.Geofences is not null, () => …)`).
- **Description**: The `CreateRouteRequestValidator` chains a `RuleForEach(req => req.Geofences!.Polygons).SetValidator(new GeofencePolygonValidator())` block but only enforces `NotEmpty` on the collection. There is **no upper bound on `Geofences.Polygons.Count`**. The parent collection `Points` IS capped (`MaxPoints = 500` at line 27) — the polygons collection is the only nested list-bearing field on this endpoint without a cap.
@@ -61,7 +61,7 @@
- `MaxPolygons = 50` (consistent with the historical use case — a route is unlikely to need more than a handful of geofence rectangles for AOI restriction).
- `MaxPolygons = MaxPoints = 500` (consistent with the sibling `Points` cap on the same DTO).
- The matching `cumulative_review_batches_01-04_cycle8_report.md` already enumerates `points.Count <= 500` (route), `items.Count <= 100` (UAV upload), `coords.Count <= 1000` (tile inventory, cycle 7) as bounded — the missing entry for `geofences.polygons` is the one inconsistency.
- **Status**: open — file as a cycle-9 follow-up under AZ-809 (or a sibling child ticket). Not release-blocking for cycle 8 itself: exploitation requires an authenticated caller with a valid GPS-permission-less JWT — the same threat model already had access to the cycle-7-pre-existing `inventory.Tiles.Count` cap, so the marginal new exposure is moderate, not catastrophic. But it MUST be fixed before any untrusted-tenant exposure is added to the route endpoint.
- **Status**: **resolved in cycle 8 (Step-14 follow-up)**. Cap added: `MaxPolygons = 50` constant + `.Must(polygons => polygons is null || polygons.Count <= MaxPolygons).WithMessage("…must contain at most 50 polygons.")` on the `geofences.polygons` rule. Cap chosen at 50 because geofences are AOI-restriction rectangles — realistic use is 1-10 polygons per route, 50 gives 5x headroom while bounding the validator's worst-case allocation to ~150 `ValidationFailure` objects. Tests added: `CreateRouteRequestValidatorTests.Validate_GeofencePolygonsTooMany_FailsCountRule` (unit) + `CreateRouteValidationTests.GeofencePolygonsTooMany_Returns400` (integration). Contract bumped to `route-creation.md` v1.0.1 (patch — tightens an existing range; new Inv-10 + test case). See `security_report_cycle8.md` § F-AZ809-1 § Resolution for the full disposition.
### F-AZ810-1 — `JsonException.Message` propagated to client in `UavUploadValidationFilter` (Low / A09 — Information Disclosure)
@@ -236,8 +236,6 @@ The handler's envelope checks are still reachable by any caller invoking `IUavTi
## Verdict (Phase 2)
**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). No Critical or High findings.
**PASS_WITH_WARNINGS** (post-follow-up)0 Medium open (F-AZ809-1 resolved in cycle 8) + 2 new Lows (F-AZ810-1, F-AZ810-2) + 2 cycle-7 Low carry-overs (F-AZ795-1, F-AZ795-2). No Critical or High findings.
Per the skill's verdict-logic, Medium severity yields PASS_WITH_WARNINGS — not FAIL (FAIL is reserved for Critical or High). F-AZ809-1 is exploitable only by an authenticated tenant operator with a valid JWT (the route endpoint requires `RequireAuthorization()` without a permission scope, so any tenant with API access reaches it). The Medium is contained within the cycle-8 threat model — every cycle-8 endpoint is auth-gated — but should be the **highest-priority cycle-9 follow-up**: pre-existing-class Mediums tend to be the entry vector for higher-severity issues when adjacent threat-model assumptions shift (e.g. if a future feature exposes the route endpoint to an untrusted-tenant audience).
The 2 new Lows + 2 carry-over Lows are not release-blockers in isolation; they are filed for the next cycle's follow-up batch.
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. The 2 new Lows + 2 carry-over Lows are not release-blockers in isolation; they are filed for the next cycle's follow-up batch.