# Phase 3 — OWASP Top 10 (2021) Review

**Review date**: 2026-05-19
**Scope**: SUT production code path (`src/gps_denied_onboard/**`) and the surfaces it exposes / consumes in production.
**Method**: Walk each OWASP Top 10 (2021) category against the SUT's actual surface; cite the project's threat-model constraints (`RESTRICT-OPS-*`, `NFT-SEC-*`) and the Phase 1 + Phase 2 findings.

## Surface Map (reference)

| Surface | Direction | Production exposure | Authentication | Notes |
|---|---|---|---|---|
| MAVLink to ArduPilot FC | bidirectional (C8) | UART/serial — physical wiring on UAV | MAVLink 2 message signing (Ed25519 derived passkey, AC-4.3 + D-C8-9) | Per-flight key rotation logged to FDR (NFT-SEC-03). iNav variant ships with documented residual risk. |
| Signed STATUSTEXT → GCS | outbound (C8) | UART → telemetry radio | Inherits MAVLink signing | Same channel as C8. |
| Tile download HTTPS → `satellite-provider` | outbound (C11 `TileDownloader`) | Operator-workstation network only (RESTRICT-OPS-1, no airborne path) | TLS server-cert validation (httpx defaults, NEVER `verify=False`) + sidecar SHA-256 check on each tile | Pinned base URL from `C11Config`. |
| Tile upload HTTPS → `satellite-provider` ingest | outbound (C11 `TileUploader`) | Operator-workstation only (post-landing) | TLS + Ed25519 per-flight signing key (C11 `PerFlightKeyManager`) | Public key envelope written to FDR for audit correlation. |
| Flights-service HTTPS → operator API | outbound (C12 `FlightsApiClient`) | Operator-workstation only | TLS + bearer auth token (config-injected, `_REDACTED` in logs) | Pinned base URL from `C12.flights_api_base_url`. |
| SSH from operator workstation → UAV companion | outbound (C12 `ParamikoSshSessionFactory`) | Operator-workstation only | SSH key auth + `RejectPolicy` host-key validation (pinned `known_hosts`) | Default mode is `strict`; `AutoAddPolicy` is forbidden by AZ-327. |
| FDR records → local FDR sink | local file write (all components) | Onboard tmpfs / local disk | N/A (local file) | Records are HMAC-chained per `fdr_record_schema.md`. |
| Healthcheck `python -m gps_denied_onboard.healthcheck` | inbound | Docker HEALTHCHECK probe, localhost only | N/A (process exit code) | No network surface. |
| **No inbound network listeners in production** | — | confirmed by NFT-SEC-05 — DNS blackhole + iptables OUTPUT REJECT in flight | — | This is the most important property of the threat model. |

The test-mode `mock-suite-sat` FastAPI fixture is the ONLY HTTP listener in any e2e topology; it lives in `e2e/fixtures/`, is excluded from the production Dockerfile, and runs only on the `e2e-net.internal: true` Docker network during tests.

---

## A01:2021 — Broken Access Control

**Status**: **N/A — no per-user authorization model exists.**

- The SUT is a single-tenant onboard binary that runs as one OS user inside its container. There is no notion of multiple users, roles, or per-resource access policy in production.
- The two privileged operations that DO exist (start-session, end-session for the C11 upload key; arming the takeoff state in C5) are gated by the C8 FC's own armed/disarmed state — i.e., the safety gate is the physical aircraft state machine, not an application-level RBAC check.
- The operator-side C12 service runs ONLY on the operator workstation (RESTRICT-OPS-1); it has no airborne path and no role-based authorization beyond the SSH key + flights-API bearer token tied to the workstation identity.

**Findings**: 0.

---

## A02:2021 — Cryptographic Failures

**Status**: **Pass.** All crypto choices are modern and well-implemented.

| Concern | Status | Evidence |
|---|---|---|
| Weak hashes for signing / integrity | None | All integrity = SHA-256 (sidecar, tile `content_sha256` column, `_canonical_hash.aggregate_tile_hash`). All signatures = Ed25519. |
| Plaintext secrets at rest | None | C8 MAVLink signing passkey loaded from tmpfs-mounted runtime secret (production) or `MAVLINK_SIGNING_PASSKEY_FILE` Docker secret (test). C11 signing key is ephemeral per flight. C12 auth token is config-injected (no on-disk plaintext in production beyond the operator-side secret store). |
| Hardcoded keys / salts | None | Phase 2 SAST: 0 hardcoded credentials. All key material is generated per-flight via `Ed25519PrivateKey.generate()` or `secrets.token_bytes(...)`. |
| TLS verification disabled | None | Phase 2 SAST: 0 `verify=False` matches; all `httpx.Client(...)` constructions use defaults (verify ON). |
| Secret zeroisation on session end | Best-effort with documented residual | `c11_tile_manager/signing_key.py:340-365` zeros the project-controlled `bytearray` mirror of the secret. OpenSSL-side buffer freed via `Ed25519PrivateKey` refcount drop. Documented residual: bounded by upload-session lifetime + RESTRICT-OPS-1 (operator workstation no-swap). AZ-318 Risk-1. |
| Random source quality | Cryptographically secure | All key generation uses `secrets.token_bytes` or `cryptography.hazmat`'s built-in CSPRNG. No `random.random()` on security paths. |
| CVE exposure in crypto libs | 1 informational (Phase 1 F4) | `cryptography==42.0.8` has 4 GHSA advisories that touch `pkcs12`, OpenSSL FIPS provider config, and a CMS recipient bug — NONE of which are reachable from the SUT's actual usage (X.509 cert validation via httpx; Ed25519 sign/verify). Bump-when-convenient. |

**Findings**: 0 (the Phase 1 F4 informational item is recorded there; not duplicated here).

---

## A03:2021 — Injection

**Status**: **Pass.** Cleared in Phase 2.

- **SQL injection**: 0 findings. All 19 `cursor.execute(...)` sites in C6 use psycopg `%s` parameterized placeholders.
- **Command injection**: 0 findings. 1 `subprocess.run` (TensorRT `trtexec`) — list args, `shell=False`, no untrusted input.
- **OS-path / file injection**: 0 findings. All `Path(...)` / `open(...)` calls take SUT-managed paths; no `..` traversal patterns matched.
- **No template / XSS surface** (no HTML output).

**Findings**: 0.

---

## A04:2021 — Insecure Design

**Status**: **Pass — the design IS the security boundary.**

The SUT's defining design property is "the airborne process has no network egress". That property is enforced at THREE layers:

1. **Architectural** — no component constructs an `httpx.Client` or opens a socket on the airborne path. The C11 downloader / uploader and C12 flights-client are wired ONLY by the operator-workstation composition root (`compose_root_operator`), not the airborne `compose_root_airborne`.
2. **Operational (RESTRICT-OPS-1)** — airborne container ships with iptables OUTPUT REJECT + DNS blackhole.
3. **Test (NFT-SEC-02 / NFT-SEC-05)** — the airborne process is run inside a network namespace with no route to `satellite-provider`'s host; any attempted outbound TCP connection is a release-blocking test failure. The harness runs this test as part of every CI build.

| Design constraint | Where enforced | Verified by |
|---|---|---|
| No inbound network listeners on airborne path | `compose_root_airborne` does not wire any FastAPI / uvicorn / grpc server | NFT-SEC-05 |
| No outbound network egress on airborne path | iptables + DNS blackhole + arch-level component placement | NFT-SEC-02 + NFT-SEC-05 |
| Tile downloads happen ONLY at operator workstation (pre-flight) | `compose_root_operator` is the only wiring that includes `build_tile_downloader` | Architecture review + integration test FT-N-01 (operator) vs FT-P-17 (airborne) |
| Tile uploads happen ONLY post-landing on operator workstation | Same — `build_tile_uploader` is `compose_root_operator` only | NFT-SEC-01 |
| MAVLink signing key generation happens on-bench, not at runtime | C8 documentation + boot sequence diagram | NFT-SEC-03 |
| Strategy implementations are EXCLUDED from the production binary at compile time | `BUILD_*` flags gate the import in `runtime_root/*_factory.py` | CI pipeline gate (`pyproject.toml` `[tool.gpsdo.build-flags]`) |

**Findings**: 0.

**Defensive observations**:

- The "test substitute" `mock-suite-sat` is documented as **non-architectural** (`_docs/02_document/architecture.md:277`) precisely so it cannot accidentally become a production dependency. The fixture is retired the moment D-PROJ-2 lands the real ingest endpoint.
- The strategy registry is keyed by string → module path through a CLOSED whitelist (`KNOWN_*_STRATEGIES`), preventing config-controlled dynamic import vectors (Phase 2 F13).

---

## A05:2021 — Security Misconfiguration

**Status**: **Mostly pass; the configuration & infrastructure review (Phase 4, next) is the deeper check.**

- **No debug surface in production**: SUT has no FastAPI / Flask / Django app; healthcheck binary uses structured exit codes, never a stack trace. `DEBUG=1` is a developer-mode env var that only widens stderr verbosity for the CLI.
- **Pinned dependencies**: `pyproject.toml` pins every direct dep; `requirements*.lock` (Phase 4 to verify) enforces transitive pins. `opencv-python>=4.11.0.86,<4.12` band is documented in `_docs/_process_leftovers/2026-05-11_d_cross_cve_1_opencv_pin_deferred.md`.
- **Build-flag gating**: strategies excluded at compile time via `BUILD_PYTORCH_FP16_RUNTIME`, `BUILD_FAISS_INDEX`, `BUILD_VPR_*`, `BUILD_C7_TRT_FP16`, etc. — non-built strategies cannot be selected even if config is wrong.
- **Test-only secrets clearly marked**: `e2e/fixtures/secrets/mavlink-test-passkey.txt` carries a "TEST ONLY" annotation; production reads from a separate tmpfs-mounted secret.
- **TODO for Phase 4**: container user (non-root?), file ownership in image layers, env var defaults, Docker secret mount permissions, network policy on `e2e-net.internal: true`.

**Findings (Phase 3 surface)**: 0. Configuration-layer concerns are deferred to Phase 4.

---

## A06:2021 — Vulnerable and Outdated Components

**Status**: Covered in Phase 1. Summary:

| Severity (project context) | Count |
|---|---|
| Critical | 0 |
| High | 0 |
| Medium | 1 (`onnx==1.18.0` GHSA-rcgw-4cgp-9q7q — reachable only via developer-mode model loading, not the production airborne path) |
| Low | 11 |

**Findings (Phase 3-specific)**: 0 new — refer to `_docs/05_security/dependency_scan.md`.

---

## A07:2021 — Identification and Authentication Failures

**Status**: **Pass.** All authenticated surfaces have a strong scheme; no obvious bypass.

| Surface | Scheme | Defense observed |
|---|---|---|
| MAVLink ↔ FC | MAVLink 2 message signing, per-flight key | Per-flight key rotation logged to FDR (NFT-SEC-03). iNav variant cannot enable signing — documented residual risk, surfaced in C8 docs (`_docs/02_document/components/10_c8_fc_adapter/description.md`). |
| C11 upload session → `satellite-provider` | Ed25519 per-flight signing key | Public-key envelope to FDR for audit correlation. Session lifetime ≤ upload window (minutes). |
| C12 → flights-service | Bearer token | Token loaded from config (operator-side secret store), `_REDACTED` in 100% of error messages and retry logs. |
| C12 → companion SSH | SSH key + `RejectPolicy` host-key validation | `paramiko.AutoAddPolicy` is FORBIDDEN by AZ-327. Default mode `strict` rejects any unknown host. CVE-2026-44405 (Phase 1 F6, SHA-1 RSA) is materially mitigated. |
| C10 manifest verification | Ed25519 detached signature + SHA-256 of canonicalized JSON + trust-anchor public-key pinning | Fail-closed (`verify_manifest` returns `outcome=FAIL` on any deviation; never raises in the success path). |

**Findings**: 0.

---

## A08:2021 — Software and Data Integrity Failures

**Status**: **Pass.** Integrity-checking is the spine of the manifest / tile / FDR contracts.

| Artifact | Integrity check | Where |
|---|---|---|
| `Manifest.json` | Ed25519 signature (`Manifest.json.sig`) + SHA-256 sidecar (`Manifest.json.sha256`) + canonical-JSON hashing of the embedded tile aggregate | `c10_provisioning/manifest_verifier.py` |
| Each tile JPEG | SHA-256 sidecar (`<tile>.sha256`), enforced both at download (`tile_downloader.py`) and on read (`postgres_filesystem_store.py` SELECT + sidecar verify) | C6 + C11 |
| FDR records | HMAC-chained schema per `_docs/02_document/contracts/shared_fdr_client/fdr_record_schema.md` | `fdr_client/records.py` |
| TensorRT engine builds | Engine path includes content-hash of source ONNX + build-config; rebuild on mismatch | `c7_inference/tensorrt_runtime.py` |
| Replay fixtures | `e2e/fixtures/sitl-replay-fixture-*` ship with a manifest builder that derives SHA-256 over every artefact | `e2e/fixtures/builders/...` |

**Software-update integrity**: out-of-scope for the onboard binary; operator-workstation provisioning is handled by the parent suite (D-PROJ-1) which Phase 4 will review at the Dockerfile / image layer.

**Findings**: 0.

---

## A09:2021 — Security Logging and Monitoring Failures

**Status**: **Pass — disciplined structured logging with security-event coverage.**

| Concern | Status | Evidence |
|---|---|---|
| Security-relevant events emit a structured FDR record | Yes | `c11.upload.session.key.public`, `c11.upload.session.key.zeroised`, `c11.upload.signature_rejected`, `c12.flights.fetch.failed`, `c10.manifest.verify.*`, `c8.mavlink.signing.*` |
| Failures (auth, integrity, transport) emit ERROR-level logs with a stable `kind` field | Yes | `kind="c12.flights.fetch.failed"` + `reason∈{auth,not_found,connect:*}`, `kind="c10.manifest.verify.failed"`, `kind="c2.vpr.dim_mismatch"`, `kind="c11.upload.signature_rejected"` |
| Secrets are not logged | Yes | 13 files use the `_REDACTED` / `redact` pattern. Phase 2 confirmed: auth token, MAVLink passkey, and SSH private-key material are never logged in plaintext. |
| Logs are tamper-resistant | Best-effort | FDR records are HMAC-chained; structured logs to stderr/journal use a defined schema (`logging/structured.py`). Operator-side log retention is a parent-suite concern. |
| Monitoring / alerting plumbed | Out of scope | Parent-suite responsibility — `_docs/02_document/deployment/observability.md` documents what the SUT emits, not where it is shipped. |

**Findings**: 0.

---

## A10:2021 — Server-Side Request Forgery (SSRF)

**Status**: **Pass.** No SUT code accepts a URL/host parameter from external input.

- `c11_tile_manager/{tile_downloader,tile_uploader}.py` build URLs by concatenating a config-pinned `base_url` with internally-derived path segments (`zoom/x/y.jpg`, `upload/<flight_id>/<tile_uuid>`). No external `?url=...` parameter or redirect-following from untrusted source.
- `c12_operator_orchestrator/flights_api/httpx_client.py` uses `client.get(url, ...)` where `url` is built from `self._config.flights_api_base_url` (config-pinned) + a derived path segment containing the validated `flight_id` UUID. No external host control.
- `httpx.Client` defaults — `follow_redirects=False`. SSRF via 30x redirect to internal addresses is blocked unless the caller explicitly opts in (no SUT caller does).
- DNS resolution happens against the operator's resolver only; in production (airborne) DNS is blackholed.

**Findings**: 0.

---

## Cross-Reference Index

| Source | Phase 3 §  | Note |
|---|---|---|
| `_docs/05_security/dependency_scan.md` | A06, A02 | Phase 1 — 12 vulnerabilities triaged |
| `_docs/05_security/static_analysis.md` | A03, A02 | Phase 2 — 1 informational hardening recommendation |
| `_docs/02_document/architecture.md` § "Operating envelopes" | A04 | RESTRICT-OPS-1 / NFT-SEC-05 enforcement |
| `_docs/02_document/tests/security-tests.md` | A04, A07, A08 | NFT-SEC-01..05 test contract |
| `_docs/_process_leftovers/2026-05-11_d_cross_cve_1_opencv_pin_deferred.md` | A06 | D-CROSS-CVE-1 deferred pin |

## Self-Verification

- [x] All 10 OWASP 2021 categories addressed with status + finding count + evidence
- [x] N/A categories (A01) explicitly justified against the threat model, not silently skipped
- [x] Findings from Phase 1 / Phase 2 cross-referenced where they map to OWASP categories rather than re-listed
- [x] Surface map at top is consistent with `_docs/02_document/architecture.md`
- [x] No new HTTP listeners / dynamic-input surfaces missed (confirmed by `Grep` for FastAPI/uvicorn/server.serve patterns)
