Files
satellite-provider/_docs/05_security/static_analysis.md
T
Oleksandr Bezdieniezhnykh 314d1dec39
ci/woodpecker/push/01-test Pipeline was successful
ci/woodpecker/push/02-build-push Pipeline was successful
[AZ-491] [AZ-492] [AZ-493] [AZ-494] [AZ-496] Cycle 3 Step 14: security audit refresh
All 5 phases refreshed against cycle-3 delta:

Phase 1 (Dependency Scan):
  - D1 RESOLVED (AZ-496): Microsoft.AspNetCore.OpenApi 8.0.21 → 8.0.25
  - D3 RESOLVED (AZ-496): JwtBearer 8.0.21 → 8.0.25
  - D4 NEW (Low, test-only): System.IdentityModel.Tokens.Jwt 7.0.3 +
    Microsoft.IdentityModel.Tokens 7.0.3 pinned in TestSupport carry
    CVE-2024-21319 (JWE DoS). Bump to ≥ 7.1.2 tracked as future PBI.

Phase 2 (Static Analysis):
  - F-AUTH-3 (Info): test runner Program.cs logs iss/aud at startup;
    production API does NOT (verified by grep).
  - F-AUTH-4 (Info): DEV-ONLY iss/aud placeholders in
    appsettings.Development.json + .env.example — by design per
    Option B for AZ-494.
  - F-DBR-1: TRUNCATE string interpolation in
    IntegrationTestDatabaseReset.cs — false positive (hard-coded
    table list).
  - F-DBR-2 (Low): TRUNCATE guard is operator-bypassable. Two-guard
    model is conservative-by-default and unit-tested.
  - F-PERF-1 (Low): perf-bootstrap --mint-only writes a 4-hour
    GPS-permission token to stdout. Operator-trusted machine assumed.

Phase 3 (OWASP Top 10):
  - A03 carries D1/D3 RESOLVED + D4 NEW.
  - A07 flips F-AUTH-2 to RESOLVED (AZ-494); residual revocation-list
    Low recorded.
  - A05 status unchanged (F-DBR-1 false positive).
  - A08 picks up F-DBR-2.

Phase 4 (Infrastructure):
  - JWT_ISSUER / JWT_AUDIENCE flow .env → compose → Kestrel config,
    same pattern as JWT_SECRET.
  - INTEGRATION_TEST_DB_RESET + ASPNETCORE_ENVIRONMENT=Testing wired
    for AZ-493 reset gate.
  - SatelliteProvider.TestSupport is IsPackable=false — never ships
    in a production container image.
  - New operational gate added to deploy runbook: grep for DEV-ONLY-
    in the rendered deploy environment must return zero hits.

Phase 5 (Security Report):
  - Verdict: PASS_WITH_WARNINGS (cycle 3 does not escalate).
  - 0 Critical, 0 High, 0 new Medium.
  - Cycle-2 F-AUTH-2 (Medium) RESOLVED; cycle-1 D1 + cycle-2 D3
    RESOLVED.

Autodev state advanced to Step 14 completed. Next: Step 15
(Performance Test, optional gate).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 03:13:04 +03:00

214 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Phase 2 — Static Analysis (SAST)
**Date**: 2026-05-11
**Scope**: All `*.cs` files in production projects (Api, Common, DataAccess, Services.*) plus Tests for false-positive triage. Configuration files (`appsettings*.json`, `docker-compose*.yml`, `Dockerfile`, `.env`).
**Method**: Pattern-based grep + targeted file review.
## Patterns checked
| Category | Pattern(s) | Verdict |
|----------|-----------|---------|
| SQL injection | `$"SELECT…"`, `+ "WHERE"`, raw `CommandText`, manual SQL string assembly | **Clean** |
| Command/process injection | `Process.Start`, `ProcessStartInfo`, `cmd.exe`, `/bin/sh`, `UseShellExecute`, `eval`-equivalent | **Clean** |
| XSS | unsanitized user input flowed to HTML or `Response.Write` | **N/A** — JSON-only API, no HTML rendering |
| Template injection | Razor / scriban / handlebars on user input | **N/A** — none used |
| Hardcoded credentials | `password = "…"`, `secret = "…"`, `token = "…"`, `apikey = "…"` in source | See findings S1, S2 |
| Weak crypto | MD5/SHA1 for passwords, `RNGCryptoServiceProvider` (deprecated), hardcoded keys | **N/A** — no password storage, no crypto code in app |
| Insecure deserialization | `BinaryFormatter`, `pickle`, untrusted JSON with type-name handling | **Clean**`System.Text.Json` with default settings; `Newtonsoft.Json` 13.0.4 used only for outbound serialization to Google session-creation endpoint (line `GoogleMapsDownloaderV2.cs`), no deserialization of untrusted inbound JSON |
| Path traversal | user input flowed into `File.Open`, `Path.Combine` | **Clean** — file paths are computed server-side from validated tile coordinates; no user-supplied path component reaches the filesystem |
| 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
### S1 — Default DB password committed in `appsettings.json` (Medium)
- **Location**: `SatelliteProvider.Api/appsettings.json:24`
- **Vulnerable code**:
```json
"DefaultConnection": "Host=localhost;Database=satelliteprovider;Username=postgres;Password=postgres"
```
- **Description**: The default (non-Development) appsettings file ships with a weak, well-known password (`postgres/postgres`). In production this string is overridden by `ConnectionStrings__DefaultConnection` in `docker-compose.yml`/env, but the file itself becomes the fallback if env-var injection ever fails or is misconfigured (silent connect-as-default behaviour).
- **Impact**: If a deployment misconfiguration drops the env override, the app silently falls back to attempting `postgres:postgres@localhost`. On a developer workstation this connects to the local Postgres container with full superuser; in production it would fail loudly only if the prod DB has different creds. Combined with finding S2 below (matching weak creds in compose file), this normalises a credential pattern that real production deployments may inherit.
- **Remediation**:
- Replace the default value with a deliberately-invalid placeholder such as `Host=__set-via-env__;Database=__;Username=__;Password=__` so a misconfiguration fails fast at startup instead of silently falling through.
- OR remove the `ConnectionStrings:DefaultConnection` key from `appsettings.json` entirely and require the env var; `Program.cs` line 2324 already throws when missing — keep that behaviour.
### S2 — Weak Postgres credentials in `docker-compose.yml` (Medium, dev-only as written)
- **Location**: `docker-compose.yml:6-7, 30`
- **Vulnerable code**:
```yaml
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
- ConnectionStrings__DefaultConnection=Host=postgres;Port=5432;Database=satelliteprovider;Username=postgres;Password=postgres
```
- **Description**: Same `postgres/postgres` credentials as S1. The compose file is labelled `Development` (`ASPNETCORE_ENVIRONMENT=Development`), so this is contained — but the file is the only compose artifact in the repo, which means anyone running `docker-compose up` on a network-reachable host immediately exposes a Postgres-with-default-creds.
- **Impact**: Postgres on `0.0.0.0:5432` (port `"5432:5432"` mapping) with `postgres/postgres` is one of the most-scanned credential pairs on the public internet. If a developer runs this on a non-laptop host (cloud VM, shared lab, etc.) the DB is trivially compromised within minutes.
- **Remediation**:
- Bind `5432` to `127.0.0.1:5432` rather than `0.0.0.0:5432` so the host firewall isn't the only protection. (Replace `"5432:5432"` with `"127.0.0.1:5432:5432"`.)
- Source `POSTGRES_USER` / `POSTGRES_PASSWORD` from the same `.env` file that already supplies `GOOGLE_MAPS_API_KEY` (line 31 already shows the pattern). Provide an `.env.example` with placeholder values and document the required vars in the README.
- The deploy/observability docs at `_docs/02_document/deployment/` already describe a secret-manager strategy for staging/prod — fold the same pattern into the dev compose.
### S3 — Latitude / longitude inputs not range-validated at the API boundary (Low)
- **Locations**:
- `SatelliteProvider.Api/Program.cs:169` — `GetTileByLatLon([FromQuery] double Latitude, [FromQuery] double Longitude, [FromQuery] int ZoomLevel, …)`
- `SatelliteProvider.Api/Program.cs:207` — `RequestRegion` validates `SizeMeters` only; `request.Latitude` / `request.Longitude` are unchecked
- `SatelliteProvider.Api/Program.cs:237` — `CreateRoute` delegates to `RouteService` which validates names but does not range-check waypoint coordinates
- **Description**: `Latitude`, `Longitude`, and (for region requests) the implicit `MaxRoutePointSpacingMeters` boundary are accepted without enforcing valid geographic ranges (`-90 ≤ lat ≤ 90`, `-180 ≤ lon ≤ 180`). `ZoomLevel` IS validated downstream by `GoogleMapsDownloaderV2` against `MapConfig.AllowedZoomLevels` — so it is fine.
- **Impact**:
- Garbage inputs (e.g. `lat=999`) propagate through `GeoUtils.WorldToTilePos` and the slippy-map math, eventually producing nonsensical tile coordinates that are persisted to `tiles` and `regions`. This is a **data-quality** issue, not a code-execution issue.
- No DoS amplification: every tile-download endpoint already enforces zoom against `AllowedZoomLevels`, so an attacker cannot use lat/lon abuse to multiply outbound Google Maps traffic beyond what zoom already bounds.
- **Remediation**: Add explicit guard clauses at the API boundary (matches the existing `SizeMeters` 100-10000 pattern):
```csharp
if (Latitude < -90 || Latitude > 90) return Results.BadRequest(new { error = "Latitude must be between -90 and 90" });
if (Longitude < -180 || Longitude > 180) return Results.BadRequest(new { error = "Longitude must be between -180 and 180" });
```
Apply uniformly to `GetTileByLatLon`, `RequestRegion`, and to each waypoint inside `CreateRoute`.
### S4 — `.env` file on developer filesystem contains an apparently real Google Maps API key (Medium — exposure depends on key reach)
- **Location**: `.env` (workspace root, **not** tracked — confirmed via `git ls-files` and `.gitignore:10`)
- **Description**: The local `.env` contains a 39-character `AIzaSy…` value matching the Google Maps API key format. The file is correctly excluded from git (line 10 of `.gitignore`) and `git log -- .env` returns no history, so the key was never committed to this repository.
- **Impact**: No repository exposure. **However**:
- If the same key is shared across developers via Slack / email / other repos, it has likely already leaked elsewhere.
- There is no `.env.example` template in the repo, which means new contributors typically request the real key via insecure channels rather than generating a fresh one.
- The key has no per-call attribution; abuse cannot be traced back to a specific developer.
- **Remediation**:
- **Rotate the key in the Google Cloud console** (out of scope for this audit — the key value is intentionally not echoed into this report).
- 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.
---
## Cycle 3 Delta (2026-05-12 — AZ-491 / AZ-492 / AZ-493 / AZ-494 / AZ-495 / AZ-496)
### Scope of this delta scan
| File | Cycle-3 task(s) | Domain |
|------|-----------------|--------|
| `SatelliteProvider.Api/Authentication/AuthenticationServiceCollectionExtensions.cs` | AZ-494 | Production auth (high-sensitivity surface) |
| `SatelliteProvider.Api/appsettings.json` + `appsettings.Development.json` | AZ-494 | Configuration / secrets handling |
| `SatelliteProvider.IntegrationTests/JwtTestHelpers.cs` | AZ-491, AZ-494 | Test-side, runner-only |
| `SatelliteProvider.IntegrationTests/IntegrationTestDatabaseReset.cs` | AZ-493 | Test-side; destructive DB op (TRUNCATE) gated by two-guard model |
| `SatelliteProvider.IntegrationTests/PerfBootstrap.cs` | AZ-492, AZ-494 | Test-side CLI subcommand (mint token, write JPEG fixture) |
| `SatelliteProvider.IntegrationTests/Program.cs` | AZ-491..AZ-494 | Test-side bootstrap |
| `SatelliteProvider.TestSupport/JwtTokenFactory.cs` | AZ-491, AZ-494 | Test-side, runner-only |
| `SatelliteProvider.TestSupport/IntegrationTestResetGuard.cs` | AZ-493 | Test-side; safety-guard logic |
| `SatelliteProvider.Tests/Authentication/AuthenticationServiceCollectionExtensionsTests.cs` | AZ-487, AZ-494 | Test-side unit |
| `SatelliteProvider.Tests/TestSupport/IntegrationTestResetGuardTests.cs` | AZ-493 | Test-side unit |
| `scripts/run-tests.sh` / `scripts/run-performance-tests.sh` | AZ-492, AZ-493, AZ-494 | Operator-side shell |
| `docker-compose.yml` / `docker-compose.tests.yml` | AZ-494 (env pass-through) | Infrastructure |
| `.env.example` | AZ-494 | Configuration template |
### Cycle-3 findings
#### F-AUTH-3 — Test runner logs `iss` / `aud` values at startup (Informational — test runner only, never in prod)
- **Location**: `SatelliteProvider.IntegrationTests/Program.cs:67` — `Console.WriteLine($"Auth : JWT_SECRET resolved ({…} bytes); iss={jwtIssuer}; aud={jwtAudience}");`
- **Description**: The integration-tests bootstrap prints the resolved iss and aud at startup. Values printed in this cycle's runs were the `DEV-ONLY-iss-admin-azaion-local` / `DEV-ONLY-aud-satellite-provider` placeholders, so no prod-value leak occurred. The production API (`SatelliteProvider.Api/Program.cs`) does NOT print iss/aud — verified by repo grep returning no hits.
- **Impact**: Only meaningful if the integration test runner is somehow pointed at production env vars. The fail-fast contract makes that operator decision visible at startup (the values are visible in test logs).
- **Disposition**: Accept — Informational. Operators inspecting test logs already see the secret byte count and the iss/aud, which is appropriate for a runner whose entire job is to validate against those values. No code change needed.
#### F-DBR-1 — `TRUNCATE TABLE` via string interpolation (False Positive — hard-coded table list)
- **Location**: `SatelliteProvider.IntegrationTests/IntegrationTestDatabaseReset.cs:32` — `$"TRUNCATE TABLE {string.Join(", ", TruncateOrder)} RESTART IDENTITY CASCADE"`.
- **Description**: SAST pattern flagged string-interpolated SQL. Source analysis confirms `TruncateOrder` is a `public static readonly IReadOnlyList<string>` initialised with a hard-coded array of five literal table names; no caller-supplied input flows into the SQL string.
- **Impact**: None. SQL injection here would require an attacker to modify the source file, at which point integrity is already broken.
- **Disposition**: False positive — recorded so future scanners don't re-flag.
#### F-DBR-2 — Destructive `TRUNCATE` action protected only by two soft guards (Low — operator-controlled, deliberate trade-off)
- **Location**: `SatelliteProvider.TestSupport/IntegrationTestResetGuard.cs:11-36` + `SatelliteProvider.IntegrationTests/IntegrationTestDatabaseReset.cs:24-37`.
- **Description**: The reset runs only when (a) `ASPNETCORE_ENVIRONMENT == "Testing"` AND (b) the Npgsql Host is one of `postgres` / `localhost` / `127.0.0.1`. An operator who sets `ASPNETCORE_ENVIRONMENT=Testing` and SSH-tunnels a production Postgres to `localhost:5432` could trick the guard.
- **Impact**: Loss of all `tiles`, `regions`, `routes`, `route_points`, `route_regions` rows on the targeted database.
- **Mitigations in place**: the cycle-3 spec deliberately preferred Host allowlist over DB-name pattern (per the AZ-493 review's "Spec-vs-reality" note); both DB-name and Host checks are cheap to add together if the operator surface grows. The guard is unit-tested (`IntegrationTestResetGuardTests`) with representative production hostnames (`prod-db-cluster-1.example.com`, etc.) to confirm they're rejected.
- **Disposition**: Accept — Low. The guard is conservative-by-default; bypassing requires deliberate operator action (env var + tunnel). Future PBI: add a third guard requiring an explicit `INTEGRATION_TEST_DB_RESET_CONFIRM=I-UNDERSTAND-THIS-TRUNCATES` env var when the guard runs against `localhost` from outside Docker.
#### F-PERF-1 — Perf-bootstrap mint subcommand writes a 4-hour `GPS`-permission token to stdout (Low — operator-controlled CLI, no network exposure)
- **Location**: `SatelliteProvider.IntegrationTests/PerfBootstrap.cs:21-48`.
- **Description**: `dotnet <integration-tests.dll> --mint-only` prints a 4-hour HS256 token with `permissions: GPS` claim to stdout. The token grants the same access as a production-issued `GPS` admin token for the lifetime window. The token bytes flow through the operator's shell history, terminal scrollback, and any process accounting logs.
- **Impact**: An attacker with read access to the operator's machine within the 4-hour window could replay the token against the API.
- **Mitigations in place**: lifetime is bounded to 4 hours (vs. e.g. 24 hours that would be tempting for "convenient perf runs"). The token is minted against `JWT_SECRET` from `.env` — same trust boundary as a developer's local dev setup. Operators are expected to run the perf script on a trusted machine.
- **Disposition**: Accept — Low. Future hardening: pipe the token to `xargs` / process substitution so it never lands in the shell history; consider mounting `JWT_SECRET` via a Docker secret rather than an env var when running the perf harness inside CI.
#### F-AUTH-4 — DEV-ONLY iss/aud placeholders committed to `appsettings.Development.json` + `.env.example` (Informational — by design, AZ-494 Option B)
- **Location**: `SatelliteProvider.Api/appsettings.Development.json` (`DEV-ONLY-iss-admin-azaion-local` / `DEV-ONLY-aud-satellite-provider`); `.env.example` (same placeholders).
- **Description**: AZ-494 (Option B per user decision) deliberately ships DEV-ONLY placeholder values in development config so local dev / docker-compose flows work without operator setup. Production config (`appsettings.json`) ships with empty values, triggering the fail-fast contract.
- **Impact**: None in production (the empty values guarantee a startup failure before any token validates). In development, the placeholders are clearly tagged with `DEV-ONLY-` prefix so a grep can surface them at any time.
- **Disposition**: Accept — by design. This is the explicit Option B trade-off the user selected over Option A (postpone) and Option C (hard-code prod values).
### Resolved this cycle
- **F-AUTH-2** (cycle 2): `iss` / `aud` not validated. **RESOLVED in AZ-494** — `ValidateIssuer = true` + `ValidateAudience = true` wired against env-sourced values with fail-fast startup. Verified at the source (`AuthenticationServiceCollectionExtensions.cs:37-40`).
### Patterns NOT triggered by cycle-3 changes
- **Injection**: SQL injection ✗ (only TRUNCATE with hard-coded table names — F-DBR-1 false positive). Command injection ✗ (no `Process.Start` / `exec` / `shell=True`). XSS ✗ (no HTML rendering paths added). Template injection ✗.
- **Cryptographic Failures**: no new hashing or encryption code; HS256 unchanged from AZ-487.
- **Insecure Deserialization**: ImageSharp decode path unchanged from cycle 2; no new `JsonSerializer.Deserialize<>` against attacker input.
## Self-verification
- [x] All production source directories scanned (Api, Common, DataAccess, Services.TileDownloader, Services.RegionProcessing, Services.RouteManagement)
- [x] All cycle-3 test-side surfaces scanned (TestSupport, IntegrationTests, Tests)
- [x] Each finding has file path and line number
- [x] False positives from test files explicitly distinguished (`GlobalExceptionHandlerTests.cs:23` "leakySecret" is a positive control); F-DBR-1 also classified as false-positive with rationale
- [x] No real secret values printed in this report (S4 is described without echoing the key; F-AUTH-4 cites placeholder values that are public-by-design)
- [x] Cycle-3 surfaces (`AddSatelliteJwt` iss/aud extension, `IntegrationTestDatabaseReset`, `PerfBootstrap`, two-guard logic) all reviewed; findings either documented above or explicitly cleared