[AZ-487] [AZ-488] security: cycle 2 delta audit (PASS_WITH_WARNINGS)

Step 14 (Security Audit) for cycle 2 — delta scan against the cycle-1
baseline. Verdict remains PASS_WITH_WARNINGS; no Critical/High.

Scope: JWT auth boundary (AZ-487) and UAV multipart upload + ImageSharp
decode of attacker-controlled bytes (AZ-488). Both new packages
(JwtBearer 8.0.21, ImageSharp 3.1.11 in Services.TileDownloader)
checked.

Cycle-2 delta:
* 0 Critical / 0 High
* 2 Medium: F-AUTH-2 (iss/aud not validated — by design until admin
  team publishes values, AZ-487 § Constraints), F-UAV-1 (ImageSharp
  decode now runs on attacker-controlled bytes — mitigations
  sufficient; pin to GHSA subscribe-and-bump policy).
* 4 Low: F-AUTH-1 (DEV-ONLY secret in appsettings.Development.json —
  accepted), F-AUTH-3 (rate-limit gap extends to 401 floods — folds
  into cycle-1 I3), F-UAV-2 (JsonDocument.Parse on signature-validated
  claims — bounded by Kestrel header cap), D3 (JwtBearer shares D1
  patch line).
* 1 Informational: F-UAV-3 (reject reasons disclose gate structure —
  accepted UX trade-off; documented in contract).

OWASP refresh: A01 / A07 move from N/A (with caveat) to
PASS_WITH_WARNINGS (per-tenant authz absent; iss/aud + revocation
gaps tracked).

Pre-deploy operational gate added: deploy pipeline must verify
JWT_SECRET != DEV-ONLY placeholder before promoting api.

Artifacts: dependency_scan.md, static_analysis.md, owasp_review.md,
infrastructure_review.md, security_report.md — all appended with a
"Cycle 2 Delta" section preserving cycle-1 finding IDs.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-12 00:13:58 +03:00
parent e3cd388577
commit 5214a4a647
6 changed files with 200 additions and 12 deletions
+50
View File
@@ -19,6 +19,10 @@
| Sensitive data in logs | passwords, API keys, tokens, PII in log statements | **Clean**`GlobalExceptionHandler.cs` logs only `Method`, `Path`, `correlationId`; client gets a generic 500 + correlationId. `CorsConfigurationValidator` warning (`PermissiveDefaultWarning`) does not include secrets. There is a deliberate test fixture `GlobalExceptionHandlerTests.cs:23` that uses `"Connection string Host=secret-db;Password=hunter2 failed at line 42"` to verify the handler does NOT echo exception messages back — this is a positive control, not a finding |
| Verbose error responses | stack traces or internal details returned to clients | **Clean**`GlobalExceptionHandler` returns RFC 7807 ProblemDetails with `Detail = "An unexpected error occurred. Use the correlationId to look up the server log entry."` |
| Input validation | numeric ranges, geo coordinates, enum-like strings | See finding S3 |
| Hardcoded credentials (cycle 2 delta) | `Jwt:Secret` value in `appsettings*.json` | `appsettings.Development.json` ships a clearly-tagged DEV-ONLY placeholder; `appsettings.json` ships `""`. `JWT_SECRET` env-var overrides both. See cycle-2 finding F-AUTH-1. |
| Authentication / authorization (cycle 2 delta) | endpoint-level Authorize, custom requirement handlers, claim parsing | `Program.cs` applies `.RequireAuthorization()` on every existing endpoint and the GPS-permission policy on the new `/api/satellite/upload`. `PermissionsAuthorizationHandler` uses `string.Equals(..., Ordinal)` — no substring / case-confusion bypass. See cycle-2 findings F-AUTH-2 .. F-AUTH-4. |
| Multipart binary input (cycle 2 delta) | uploaded bytes flowing into image decode / file write | `UavTileQualityGate` runs magic-byte check before ImageSharp, wraps decode in scoped `try/catch` for `UnknownImageFormatException` / `InvalidImageContentException`. File path is built from integer coords only via `UavTileUploadHandler.BuildUavTileFilePath`. See cycle-2 finding F-UAV-1. |
| Untrusted JSON via claims (cycle 2 delta) | `JsonDocument.Parse(claim.Value)` in `PermissionsAuthorizationHandler` | Tokens are signature-validated *before* the handler runs, so the JSON parsed here is already framework-validated bytes from a verified token. Token size is bounded by Kestrel header limits. See cycle-2 finding F-UAV-2. |
## Findings
@@ -82,6 +86,52 @@
- Add `.env.example` to the repo with `GOOGLE_MAPS_API_KEY=replace-with-your-own-key-from-cloud-console` and reference it in the README setup section.
- Configure Google Cloud key restrictions: HTTP referrer allowlist (for browser keys) or IP allowlist (for server keys), and per-API quotas. Optional: per-developer keys.
---
## Cycle 2 Delta Findings (AZ-487 + AZ-488)
### F-AUTH-1 — Dev JWT secret is committed to `appsettings.Development.json` (Low — accepted by design)
- **Location**: `SatelliteProvider.Api/appsettings.Development.json:14` — `"Secret": "DEV-ONLY-DO-NOT-USE-IN-PROD-replace-with-real-secret-via-JWT_SECRET-env-var"`.
- **Description**: A 73-byte placeholder labelled DEV-ONLY ships in the repo. The value is clearly tagged; `ResolveSecretOrThrow` in `AuthenticationServiceCollectionExtensions.cs:43` reads `JWT_SECRET` from the environment first and only falls back to config when it is unset, so a production deploy with `JWT_SECRET` set overrides it.
- **Impact**: Cosmetic only — the placeholder is not a usable production secret (it is published on every git clone and would be rejected by any token verifier already in the wild). A careless operator who copies the file verbatim into prod and forgets to set `JWT_SECRET` would still pass the ≥32-byte gate, so the secret would *work* locally — that is the dependency to monitor.
- **Disposition**: Accept. Mitigation: the `DEV-ONLY-DO-NOT-USE-IN-PROD` prefix is the operator-readable warning; the deploy skill must verify `JWT_SECRET` is set before promotion.
### F-AUTH-2 — JWT issuer / audience are not validated (Medium — by design, until admin team defines values)
- **Location**: `SatelliteProvider.Api/Authentication/AuthenticationServiceCollectionExtensions.cs:31-32` — `ValidateIssuer = false`, `ValidateAudience = false`.
- **Description**: Per the suite contract `suite/_docs/10_auth.md`, expected `iss` / `aud` values are not yet defined. The validator therefore accepts any HS256 token signed with the correct shared secret — including tokens minted by other services in the suite that share the secret. This is a horizontal-trust risk: any service that holds `JWT_SECRET` can mint tokens accepted by satellite-provider as if they came from the admin API.
- **Impact**: Bounded by the secret-distribution policy. Within the trust boundary documented in cycle 1's A01 caveat ("internal/trusted-network service") this is acceptable.
- **Remediation (follow-up, NOT this cycle)**: When the admin team publishes `iss` / `aud` values, flip `ValidateIssuer = true` + `ValidIssuer = "<admin-iss>"` and the audience equivalent in `AddSatelliteJwt`. AZ-487 § Constraints already flags this as a small follow-up.
### F-AUTH-3 — No rate limiting on 401-producing paths (Low — recurrence of cycle-1 I3)
- **Location**: every `/api/satellite/*` endpoint after the AZ-487 `.RequireAuthorization()` middleware.
- **Description**: An attacker can flood `Authorization: Bearer <random>` requests; each one triggers an HMAC verification (cheap, but non-zero) and an HTTP 401 response. This re-uses the cycle-1 I3 finding ("no inbound rate limiting on any HTTP endpoint") — the JWT layer didn't introduce a new vulnerability, but it did add a new cheap-to-trigger 401 surface that magnifies I3.
- **Disposition**: Track under existing I3 remediation (wire `Microsoft.AspNetCore.RateLimiting`). No separate Jira.
### F-UAV-1 — ImageSharp decode on attacker-controlled bytes (Medium — exposure increase, mitigations sufficient today)
- **Location**: `SatelliteProvider.Services.TileDownloader/UavTileQualityGate.cs:60-95` — `Image.Identify` (Rule 3) and `Image.Load<L8>` + `Mutate(ctx => ctx.Resize)` (Rule 5).
- **Description**: Pre-AZ-488, ImageSharp only decoded responses from the Google Maps tile CDN (trusted origin). AZ-488 added a second call site that decodes arbitrary `POST /api/satellite/upload` payloads. Current ImageSharp 3.1.11 is patched (see cycle-2 dependency-scan finding F-DEPS-UAV); the change here is *exposure*, not a present vulnerability.
- **Mitigations in place**:
- Rule 1 magic-byte gate runs before any ImageSharp call (`FF D8 FF` prefix required).
- Rule 2 caps per-item size at 5 MiB; Kestrel + FormOptions cap the envelope at `MaxBatchSize × MaxBytes`.
- Decode is wrapped in `try { … } catch (UnknownImageFormatException) { … } catch (InvalidImageContentException) { … }` — malformed JPEGs produce a structured `INVALID_FORMAT` reject; no unhandled exception reaches the client.
- **Remediation**: Subscribe to `SixLabors.ImageSharp` GHSA advisories; bump within 7 days of a patch. Sandboxing (separate process / libvips + seccomp) is not warranted at the current trust boundary but should be reconsidered if the endpoint is exposed publicly. Recorded as recurring follow-up.
### F-UAV-2 — `JsonDocument.Parse` invoked on token-supplied claim values (Low — bounded by Kestrel header limits)
- **Location**: `SatelliteProvider.Api/Authentication/PermissionsRequirement.cs:84-111` — `JsonDocument.Parse(claim.Value)` when the `permissions` claim arrives as a JSON-array string.
- **Description**: `JsonDocument.Parse` has no built-in depth or size limit. A maliciously-shaped permissions claim (e.g. deeply-nested array) would consume CPU/heap during parsing. The token has already passed HS256 signature validation by the time the handler runs, so this is only exploitable by a party that holds `JWT_SECRET` — i.e. another suite service or an admin-team principal — and only inside the issued-token-size window (bounded by Kestrel's `MaxRequestHeadersTotalSize`, default 32 KiB).
- **Disposition**: Accept. The combination of `RequireSignedTokens = true` + header-size cap + ordinal-only string comparison makes a practical exploit prohibitive. Future hardening: pass `JsonDocumentOptions { MaxDepth = 8 }` to `JsonDocument.Parse` and reject claims longer than e.g. 8 KiB before parsing.
### F-UAV-3 — Reject reasons disclose gate structure (Informational — accepted trade-off)
- **Location**: `SatelliteProvider.Services.TileDownloader/UavTileQualityGate.cs` — each rule returns a distinct enum code.
- **Description**: A client (or attacker who can present a `GPS`-permission token) can map the gate by probing inputs (1×1 black image → `WRONG_DIMENSIONS`; 1 KB JPEG → `SIZE_OUT_OF_BAND`; etc.). The thresholds are also documented in the public contract `_docs/02_document/contracts/api/uav-tile-upload.md`.
- **Disposition**: Accept — UX (helping clients self-correct) outweighs the information-hiding benefit, especially since the contract is public anyway. Flagged to keep operators aware: rule thresholds are NOT a security boundary; do not move secrets into reject details.
## Self-verification
- [x] All production source directories scanned (Api, Common, DataAccess, Services.TileDownloader, Services.RegionProcessing, Services.RouteManagement)