mirror of
https://github.com/azaion/satellite-provider.git
synced 2026-06-21 21:21:15 +00:00
61612044fb
Wrap up cycle 5 verification + documentation: - Steps 10/11 wrap-up reports (implementation_completeness + implementation_report) for the AZ-503-foundation + AZ-504 batch. - Step 12 test-spec sync: AZ-503-foundation/AZ-504 ACs appended; AZ-505 deferred ACs recorded. - Step 13 update-docs: architecture, data-model, glossary, module- layout, uav-tile-upload contract (v1.1.0), DataAccess + Services + Tests module docs synced; new common_uuidv5.md module doc. - Step 14 security audit: PASS_WITH_WARNINGS; 0 new Critical/High; 2 new Low informational (F1 flightId provenance, F2 pgcrypto deploy gap). - Step 15 performance test: PASS_WITH_INFRA_WARNINGS; PT-08 passed twice (AZ-504 fix verified); PT-01/02 failed due to recurring local Docker/colima DNS cold-start (not an app regression). Cycle-3 perf-harness leftover stays OPEN with replay #5 documented. - Autodev state moved to Step 16 (Deploy). Co-authored-by: Cursor <cursoragent@cursor.com>
142 lines
11 KiB
Markdown
142 lines
11 KiB
Markdown
# Static Analysis (Cycle 5)
|
|
|
|
**Date**: 2026-05-12
|
|
**Mode**: Delta scan
|
|
**Scope**: Cycle-5 delta over the cycle-3 static analysis (`_docs/05_security/static_analysis.md`). Cycle 4 was source-edit-free for SAST surfaces (AZ-500 was a runtime/package bump only); cycle 5 reintroduces real source edits and is the first SAST delta since cycle 3.
|
|
|
|
## Files in Scope
|
|
|
|
AZ-503-foundation (production code only — test code excluded from SAST per the cycle-3 policy):
|
|
|
|
- `SatelliteProvider.Common/Utils/Uuidv5.cs` (new)
|
|
- `SatelliteProvider.Common/DTO/UavTileMetadata.cs` (+1 nullable `Guid? FlightId` property)
|
|
- `SatelliteProvider.DataAccess/Migrations/014_AddTileIdentityColumns.sql` (new — SQL migration)
|
|
- `SatelliteProvider.DataAccess/Models/TileEntity.cs` (+4 properties)
|
|
- `SatelliteProvider.DataAccess/Repositories/TileRepository.cs` (UPSERT key change; one new column list element on UPDATE)
|
|
- `SatelliteProvider.Services.TileDownloader/TileService.cs` (`BuildTileEntity` computes deterministic Id / LocationHash / ContentSha256)
|
|
- `SatelliteProvider.Services.TileDownloader/UavTileUploadHandler.cs` (`PersistAsync` reads `metadata.FlightId`; `BuildUavTileFilePath` accepts `Guid? flightId`)
|
|
|
|
AZ-504:
|
|
|
|
- `scripts/run-performance-tests.sh:416-417` (two `grep -o` counters wrapped in `{ … || true; }`)
|
|
|
|
## Injection
|
|
|
|
### SQL injection
|
|
|
|
`TileRepository.InsertAsync` uses Dapper parameterized SQL throughout — no string interpolation of user-controlled values into the SQL text. `flight_id`, `location_hash`, `content_sha256`, `legacy_id` are bound via `@flightId`, `@locationHash`, `@contentSha256`, `@legacyId`. The new `COALESCE(flight_id, '00000000-...'::uuid)` predicate uses a hardcoded zero-UUID literal — not user-controlled.
|
|
|
|
Migration 014's PL/pgSQL `pg_temp.uuidv5` function takes `namespace_uuid uuid, name text` parameters; the only `name` value passed is `tile_zoom::text || '/' || tile_x::text || '/' || tile_y::text` over data already in the table. The migration runs under DbUp's bootstrap path (server-trusted, no user input).
|
|
|
|
**Finding**: none.
|
|
|
|
### Command injection
|
|
|
|
No `Process.Start`, `Shell.Execute`, or `subprocess`-equivalent calls were introduced. `Uuidv5.cs` uses pure BCL (`SHA1.HashData`, `BinaryPrimitives`). `UavTileUploadHandler.PersistAsync` writes a file via `File.WriteAllBytesAsync` — no shell.
|
|
|
|
The AZ-504 shell-script edit wrapped two `grep -o` invocations in `{ … || true; }`. The wrapped commands were already there in cycle 3; AZ-504 only added the `|| true` guard. No new shell-evaluated input.
|
|
|
|
**Finding**: none.
|
|
|
|
### Path traversal
|
|
|
|
The new `UavTileUploadHandler.BuildUavTileFilePath(StorageConfig, int tileZoom, int tileX, int tileY, Guid? flightId)` constructs an on-disk path. All inputs are integer-typed (`tileZoom`, `tileX`, `tileY`) or `Guid?`. The `flightId` segment is rendered via `flightId.Value.ToString("D", CultureInfo.InvariantCulture)` which **always** emits the 36-character hyphenated form (`xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`). It is structurally impossible to inject `..`, `/`, `\`, or null bytes into a `Guid.ToString("D")`. Anonymous uploads use the literal compile-time constant `AnonymousFlightSegment = "none"`. Integer-typed coordinates similarly cannot carry traversal characters.
|
|
|
|
Path joining uses `Path.Combine` which handles platform separator normalization. The deletion case `rm -rf ./tiles/uav/{flight_id}/` is operator-driven, not API-driven — there is no endpoint that takes a flight_id and deletes anything.
|
|
|
|
**Finding**: none.
|
|
|
|
### Template / formatting injection
|
|
|
|
`Uuidv5.Create(Guid namespaceId, string name)` accepts a `string name`. The string is hashed (SHA-1 of namespace bytes + UTF-8 name), not interpolated into any template. The hash output is then post-processed into a `Guid`. No injection surface.
|
|
|
|
**Finding**: none.
|
|
|
|
## Authentication & Authorization
|
|
|
|
AZ-503 did NOT add a new endpoint and did NOT change the existing auth/permission policies on `POST /api/satellite/upload` (still: JWT (HS256) + `GPS` permission claim, owned by AZ-487 + AZ-488). The optional `flightId` per-item metadata field is on the inside of the JWT-protected envelope; an unauthenticated caller cannot reach it.
|
|
|
|
The new `metadata.flightId` field is **not** used as an authorization key. It is an opaque identifier for evidence-isolation. The handler does not check "does this principal own this flight" — that is intentional for v1.1.0 of the upload contract (documented in `_docs/02_document/contracts/api/uav-tile-upload.md`). A future contract bump may add per-flight ownership; for now, any caller with the `GPS` permission can write under any `flightId`.
|
|
|
|
**Finding**: F1-cy5 — Low (informational), Insecure Design.
|
|
- Location: `_docs/02_document/contracts/api/uav-tile-upload.md` v1.1.0 + `UavTileUploadHandler.PersistAsync`
|
|
- Description: The `metadata.flightId` field is accepted from authenticated callers without verifying the caller "owns" or is authorized to write under that flight identifier. By design (v1.1.0). Two adversarial cases:
|
|
1. A compromised UAV credential could falsely attribute its uploads to a different flight_id (impersonation on the evidence chain).
|
|
2. A legitimate UAV credential could falsely attribute its own uploads to a competing operator's flight_id (false-flag).
|
|
- Impact: Evidence-chain integrity is **not** cryptographically enforced for the flight_id field; downstream consumers should not treat `tiles.flight_id` as proof of provenance. They MUST cross-reference flight_id against an authoritative flight registry (out of this workspace's scope) before drawing conclusions.
|
|
- Remediation: Recorded as a design constraint, not a defect. If a future cycle requires per-flight ownership, three options:
|
|
- (a) Issue per-flight JWTs (subject = flight_id; reject mismatched `metadata.flightId` server-side).
|
|
- (b) Have the Admin API mint a short-lived flight-scoped permission claim, e.g. `permissions: ["GPS:flight=<uuid>"]`.
|
|
- (c) Move flight_id derivation server-side from a trusted claim (`token.sub` or `token.flight_id`).
|
|
- Severity rationale: Low because (i) no current consumer treats flight_id as authenticated provenance, (ii) the GPS permission is gated upstream by the Admin API per `suite/_docs/10_auth.md`, (iii) the surface only exists when an attacker already holds a valid GPS-permissioned JWT.
|
|
|
|
## Cryptographic Failures
|
|
|
|
### SHA-1 in `Uuidv5.cs`
|
|
|
|
`Uuidv5.Create` uses `SHA1.HashData(...)` — but **not** as a cryptographic primitive. It is the RFC 9562 §5.5 algorithm requirement for UUIDv5 generation; the result is a stable, deterministic 128-bit handle used as a database key and an on-disk path component. SHA-1's collision vulnerability (SHAttered, 2017) is irrelevant here because:
|
|
|
|
1. The UUIDv5 result is **not** used as a content integrity check — `content_sha256` (SHA-256) is the content-integrity primitive.
|
|
2. Two different `(namespace, name)` pairs producing the same UUIDv5 would require an adversary to craft SHA-1 collisions in advance with full control over both inputs. The `namespace` is a pinned constant (`5b8d0c2e-...`); the only attacker-influenced input is the `name`, which for tile identity is `{z}/{x}/{y}/{source}/{flight_id-or-zero}`. The attacker cannot freely choose `{z}/{x}/{y}` (those are integers derived from public coordinates) or `{source}` (closed enum). The only free variable is `flight_id`. A collision-induced overwrite would require the attacker to (a) hold a GPS-permissioned JWT, (b) compute a SHA-1 collision against a target row's full canonical name, (c) submit a JPEG that matches the victim's `(z, x, y, source)`. The compute cost remains in the hundreds-of-thousands of GPU-hours range and the operational cost (a valid JWT + a forged image that bypasses the 5-rule quality gate) makes this purely theoretical.
|
|
3. The .NET implementation's `SHA1.HashData` calls into CNG / OpenSSL FIPS-validated SHA-1; the algorithm is not in-process and not subject to side-channel concerns.
|
|
|
|
**Finding**: none. SHA-1 use is RFC-mandated and documented in `_docs/02_document/modules/common_uuidv5.md` § Security with the appropriate "not a cryptographic hash for security purposes" disclaimer.
|
|
|
|
### SHA-256 in `TileService.BuildTileEntity` + `UavTileUploadHandler.PersistAsync`
|
|
|
|
`content_sha256` is computed via `SHA256.HashData(stream)` over the on-disk JPEG body. Stored in `tiles.content_sha256` (bytea). Used to detect byte-identical re-uploads (AZ-503 AC-7). SHA-256 is appropriate here.
|
|
|
|
The DB column is `bytea NULL` — legacy pre-AZ-503 rows have NULL because the migration cannot reliably re-read those file bytes (file paths rotate on every Google Maps re-download due to the timestamped legacy layout). Application code enforces `NOT NULL` for new writes. The Low-severity maintainability finding recorded in `batch_02_cycle5_report.md` covers this trade-off.
|
|
|
|
**Finding**: none (already covered by code-review at Low maintainability level).
|
|
|
|
### Plaintext storage of `JWT_SECRET`, `GOOGLE_MAPS_API_KEY`
|
|
|
|
Unchanged from cycle 3 — both come from environment variables / `.env` (gitignored). AZ-503 added no new secret.
|
|
|
|
## Data Exposure
|
|
|
|
### `content_sha256` in API responses
|
|
|
|
The `tileId` returned in `UavTileBatchUploadResponse.items[].tileId` is the deterministic UUIDv5. Is this a privacy leak? The UUID encodes `(z, x, y, source, flight_id-or-zero)` deterministically under a public namespace. An external observer with the `tileId` could verify a `(z, x, y, source, flight_id)` guess but cannot enumerate or reverse without the inputs already in hand. For UAV uploads:
|
|
- `z, x, y` are derived from `(latitude, longitude, zoom)` the client itself supplied — already known to that client.
|
|
- `source` is `"uav"` for these responses — already known.
|
|
- `flight_id` is supplied by the client (or `00000000-...` for anonymous) — already known.
|
|
|
|
So the deterministic `tileId` returned to the client tells the client only what the client already knew. **No new information leak.**
|
|
|
|
For Google Maps tiles (returned via `GET /api/satellite/tiles/latlon`), the same logic holds: the client already knows `(z, x, y)` because it constructed the request.
|
|
|
|
**Finding**: none.
|
|
|
|
### Migration backfill data exposure
|
|
|
|
Migration 014 writes `location_hash` and `legacy_id` to every existing row. Neither column is projected by `GET /api/satellite/region/{id}`, `GET /api/satellite/route/{id}`, or any public response. They are internal columns. The migration's logs (DbUp's `LogToConsole()`) emit row counts and timing — not row content.
|
|
|
|
**Finding**: none.
|
|
|
|
### `legacy_id` retention
|
|
|
|
`legacy_id` retains the pre-AZ-503 random `Guid` for forensics. This is internal data — not exposed via API surface. To be dropped in a future cycle (already noted in `data_model.md`).
|
|
|
|
**Finding**: none.
|
|
|
|
## Insecure Deserialization
|
|
|
|
`UavTileMetadata.FlightId` is deserialized from JSON via `System.Text.Json`. The target type is `Guid?` — the deserializer enforces a strict 36-character hyphenated format (or 32-char un-hyphenated) and throws `JsonException` on invalid input, which is caught at the envelope level and surfaces as HTTP 400. No type-confusion or polymorphic deserialization path was introduced.
|
|
|
|
`Uuidv5.Create` does not deserialize anything — it consumes a `string`/`ReadOnlySpan<byte>` and a `Guid`.
|
|
|
|
**Finding**: none.
|
|
|
|
## Self-verification
|
|
|
|
- [x] All AZ-503 production source files scanned
|
|
- [x] AZ-504 shell-script delta scanned
|
|
- [x] No false positives raised from test code
|
|
- [x] All findings carry file path / location
|
|
|
|
## Save action
|
|
|
|
Written to `_docs/05_security/static_analysis_cycle5.md`. Carry the cycle-3 `static_analysis.md` forward — no overlap with cycle-5 surface.
|