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

25 KiB
Raw Blame History

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 CleanSystem.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 CleanGlobalExceptionHandler.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 CleanGlobalExceptionHandler 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:
    "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:
    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:169GetTileByLatLon([FromQuery] double Latitude, [FromQuery] double Longitude, [FromQuery] int ZoomLevel, …)
    • SatelliteProvider.Api/Program.cs:207RequestRegion validates SizeMeters only; request.Latitude / request.Longitude are unchecked
    • SatelliteProvider.Api/Program.cs:237CreateRoute 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):
    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-32ValidateIssuer = 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-95Image.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-111JsonDocument.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:67Console.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-494ValidateIssuer = 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

  • All production source directories scanned (Api, Common, DataAccess, Services.TileDownloader, Services.RegionProcessing, Services.RouteManagement)
  • All cycle-3 test-side surfaces scanned (TestSupport, IntegrationTests, Tests)
  • Each finding has file path and line number
  • 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
  • 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)
  • Cycle-3 surfaces (AddSatelliteJwt iss/aud extension, IntegrationTestDatabaseReset, PerfBootstrap, two-guard logic) all reviewed; findings either documented above or explicitly cleared