mirror of
https://github.com/azaion/ui.git
synced 2026-06-21 19:21:10 +00:00
[AZ-501] [AZ-502] Cycle 2 Step 14 security audit + inline fixes
ci/woodpecker/push/build-arm Pipeline failed
ci/woodpecker/push/build-arm Pipeline failed
Security audit (5 phases) → reports under _docs/05_security/. AZ-501 (F-SAST-1, HIGH): Externalize hardcoded Google Geocode key from mission-planner/src/config.ts to VITE_GOOGLE_GEOCODE_KEY via new GeocodeService.ts; fail-soft warn when unset; STC-SEC1D static deny-list gate; +5 unit tests in tests/mission_planner_geocode.test.ts. AZ-502 (F-DEP-1, HIGH): Force vite>=6.4.2 and postcss>=8.5.10 via package.json overrides in both roots; clean reinstall clears all bun audit advisories. Test-spec sync (Step 12) + Update Docs (Step 13) deltas: AC-43, AC-44, NFT-SEC-09b, FT-P-61, FT-N-17, ripple log, batch_12 report. Pending user actions: revoke Google + OWM keys (AC-6 / AZ-499 AC-7). 229 PASS / 13 SKIP / 0 FAIL on static + fast suites. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
# Dependency Scan — Azaion UI
|
||||
|
||||
**Date**: 2026-05-12
|
||||
**Scope**: `package.json` + `bun.lock` (root) and `mission-planner/package.json` + `mission-planner/bun.lock`
|
||||
**Tool**: `bun audit v1.3.11` (the project's pinned package manager)
|
||||
**Cycle**: Phase B / Cycle 2 (post AZ-498, AZ-499)
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Severity | Count | Packages |
|
||||
|----------|-------|----------|
|
||||
| Critical | 0 | — |
|
||||
| High | 1 | `vite` (dev-server only) |
|
||||
| Moderate | 2 | `vite` (dev-server only), `postcss` (build-time, low surface) |
|
||||
| Low | 0 | — |
|
||||
|
||||
**Both roots (main `ui/` and `mission-planner/`) report the SAME advisory set** — they share the same Vite 6.x + PostCSS 8.5.x major versions.
|
||||
|
||||
## Findings
|
||||
|
||||
### F-DEP-1 — Vite Arbitrary File Read via Dev Server WebSocket — HIGH
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Advisory | [GHSA-p9ff-h696-f583](https://github.com/advisories/GHSA-p9ff-h696-f583) |
|
||||
| Package | `vite` |
|
||||
| Installed | `6.4.1` (resolved in `bun.lock`) |
|
||||
| Affected | `vite <= 6.4.1` |
|
||||
| Fix | Upgrade to `vite >= 6.4.2` (or latest 6.x), or `bun update` |
|
||||
| Found via | `bun audit` |
|
||||
| Roots | `ui/` (direct), `mission-planner/` (direct) |
|
||||
|
||||
**Production impact**: **NONE.** The Vite dev server is only used during `bun run dev` and `vitest` (test). Production runs `nginx:alpine` serving pre-built static assets from `dist/` (`Dockerfile:8-12`). The Vite WebSocket endpoint does not exist in production.
|
||||
|
||||
**Developer-machine impact**: **HIGH** in dev. An attacker on the same network as a developer running `bun run dev` (default `--host` exposes `0.0.0.0`) can read arbitrary files from the developer's filesystem via the WebSocket path traversal. Mitigation: bind dev server to `localhost` only (Vite default unless `--host` is passed).
|
||||
|
||||
**Remediation**:
|
||||
1. `bun update vite` in both roots (drops in-range to `6.4.2+`).
|
||||
2. Verify build passes (`bun run build`) and fast tests stay green (`scripts/run-tests.sh fast`).
|
||||
3. CI would-have-blocked check: add `bun audit --high` exit-code gate to `.woodpecker/build-arm.yml` (Phase B follow-up — see infrastructure review).
|
||||
|
||||
### F-DEP-2 — Vite Path Traversal in Optimized Deps `.map` Handling — MODERATE
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Advisory | [GHSA-4w7w-66w2-5vf9](https://github.com/advisories/GHSA-4w7w-66w2-5vf9) |
|
||||
| Package | `vite` |
|
||||
| Installed | `6.4.1` |
|
||||
| Affected | `vite <= 6.4.1` |
|
||||
| Fix | Upgrade to `vite >= 6.4.2` (same upgrade as F-DEP-1) |
|
||||
| Found via | `bun audit` |
|
||||
| Roots | `ui/` (direct), `mission-planner/` (direct) |
|
||||
|
||||
**Production impact**: **NONE** — same reason as F-DEP-1; production has no Vite dev server.
|
||||
|
||||
**Developer-machine impact**: **MODERATE** — path traversal on `/optimized-deps/<…>.map` paths during dev sessions.
|
||||
|
||||
**Remediation**: same upgrade as F-DEP-1 (single `bun update vite` resolves both).
|
||||
|
||||
### F-DEP-3 — PostCSS XSS via Unescaped `</style>` in CSS Stringify Output — MODERATE
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Advisory | [GHSA-qx2v-qp2m-jg93](https://github.com/advisories/GHSA-qx2v-qp2m-jg93) |
|
||||
| Package | `postcss` (transitive: `vite > postcss`) |
|
||||
| Installed | `8.5.8` (resolved in `bun.lock`) |
|
||||
| Affected | `postcss < 8.5.10` |
|
||||
| Fix | Upgrade to `postcss >= 8.5.10` (transitive — flows through `vite >= 6.4.2`) |
|
||||
| Found via | `bun audit` |
|
||||
| Roots | `ui/` (transitive), `mission-planner/` (transitive) |
|
||||
|
||||
**Production impact**: **LOW.** The advisory affects code that takes UNTRUSTED CSS as input and feeds it to PostCSS to stringify; the result is then injected into a page, allowing `</style>` breakout → XSS. In this project PostCSS only processes:
|
||||
- `src/index.css` (controlled, in-repo)
|
||||
- Tailwind-generated CSS (via `@tailwindcss/vite` 4.2.2, controlled inputs)
|
||||
- No user-supplied CSS is ever processed.
|
||||
|
||||
There is no exploit path in this codebase today. Treat as a hygiene upgrade.
|
||||
|
||||
**Build-time impact**: PostCSS runs at build time. The vulnerability surfaces only with attacker-controlled CSS input, which does not occur in this build.
|
||||
|
||||
**Remediation**: same upgrade as F-DEP-1/F-DEP-2 (transitive resolution lifts `postcss` to `>= 8.5.10`).
|
||||
|
||||
## Combined Remediation
|
||||
|
||||
A single command fixes all three findings in both roots:
|
||||
|
||||
```bash
|
||||
bun update vite # in ui/
|
||||
cd mission-planner && bun update vite
|
||||
```
|
||||
|
||||
Then re-run `bun audit` in both roots to confirm zero findings.
|
||||
|
||||
## CI Coverage Gap
|
||||
|
||||
`.woodpecker/build-arm.yml` does NOT run `bun audit` today (confirmed by file inspection). The current pipeline catches only static-analysis regressions (`scripts/run-tests.sh static`), not new CVEs entering the lockfile. This is a **MEDIUM** infrastructure finding — see `infrastructure_review.md` F-INF-1.
|
||||
|
||||
## Self-verification
|
||||
|
||||
- [x] Both `package.json` manifests scanned (`ui/` + `mission-planner/`)
|
||||
- [x] Each finding has a GHSA advisory ID
|
||||
- [x] Upgrade paths identified for the High and Moderate findings (single `bun update vite`)
|
||||
- [x] Production vs. dev impact distinguished for every finding
|
||||
@@ -0,0 +1,236 @@
|
||||
# Infrastructure & Configuration Review — Azaion UI
|
||||
|
||||
**Date**: 2026-05-12
|
||||
**Scope**: `Dockerfile`, `nginx.conf`, `.woodpecker/build-arm.yml`, `e2e/docker-compose.suite-e2e.yml`, `.env.example` files, `.gitignore`
|
||||
**Cycle**: Phase B / Cycle 2
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Severity | Count |
|
||||
|----------|-------|
|
||||
| Critical | 0 |
|
||||
| High | 0 |
|
||||
| Medium | 4 (F-INF-1 .. F-INF-4) |
|
||||
| Low | 1 (F-INF-5) |
|
||||
|
||||
All findings are pre-existing infrastructure hardening gaps — no new findings introduced by Cycle 2. Several findings here overlap with `owasp_review.md` A05/A08 entries and are cross-referenced.
|
||||
|
||||
---
|
||||
|
||||
## Container Security
|
||||
|
||||
### `Dockerfile`
|
||||
|
||||
```dockerfile
|
||||
FROM --platform=$BUILDPLATFORM oven/bun:1.3.11-alpine AS build
|
||||
WORKDIR /app
|
||||
COPY package.json bun.lock* ./
|
||||
RUN bun install --frozen-lockfile
|
||||
COPY . .
|
||||
RUN bun run build
|
||||
|
||||
FROM nginx:alpine
|
||||
ARG CI_COMMIT_SHA=unknown
|
||||
ENV AZAION_REVISION=$CI_COMMIT_SHA
|
||||
COPY --from=build /app/dist /usr/share/nginx/html
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
EXPOSE 80
|
||||
```
|
||||
|
||||
**Verified controls**:
|
||||
- Multi-stage build → only static assets land in the runtime image; no Bun, no node_modules, no source.
|
||||
- Alpine base images → minimal attack surface.
|
||||
- `--frozen-lockfile` → no transitive drift between `bun install` and what was tested.
|
||||
- `CI_COMMIT_SHA` baked in for traceability.
|
||||
- `EXPOSE 80` — port surface limited to nginx HTTP.
|
||||
|
||||
**Findings**:
|
||||
- **F-INF-5** — runs as `nginx` default `root` for the master process. The `nginx:alpine` image's default config drops worker processes to `nginx` user; the master remains `root`. Consider switching to `nginxinc/nginx-unprivileged` if the suite ingress permits a non-80 listen port. **Severity: LOW** (industry-standard pattern; minor improvement).
|
||||
- No `HEALTHCHECK` directive in the Dockerfile (the e2e compose adds one externally). For Kubernetes / external orchestration, add `HEALTHCHECK CMD wget -qO- http://localhost:80/ || exit 1`. **Severity: LOW** (operational, not security).
|
||||
|
||||
**No HIGH/MEDIUM issues with the Dockerfile itself.**
|
||||
|
||||
---
|
||||
|
||||
## CI/CD Security
|
||||
|
||||
### `.woodpecker/build-arm.yml`
|
||||
|
||||
**Verified controls**:
|
||||
- Registry credentials sourced from secrets (`from_secret: registry_token` etc.) — never in repo.
|
||||
- `docker login --password-stdin` — token not in argv (would otherwise leak via `ps`).
|
||||
- Branch-restricted (`when.branch: [dev, stage, main]`) — feature branches do NOT push to registry.
|
||||
- OCI labels (`org.opencontainers.image.revision`, `created`, `source`) stamped at build time.
|
||||
- The image tag is branch-derived (`${CI_COMMIT_BRANCH}-arm`) — production deployments pin to the SHA via OCI label.
|
||||
|
||||
**Findings**:
|
||||
|
||||
### F-INF-1 — `bun audit` not gated in CI — MEDIUM
|
||||
|
||||
**Evidence**: `.woodpecker/build-arm.yml` runs only `docker build` + `docker push`. The static-test pipeline runs in the developer's `scripts/run-tests.sh`, NOT in CI. The Cycle 2 dependency findings (F-DEP-1 vite High, F-DEP-2/F-DEP-3 Moderate) would not have failed CI.
|
||||
|
||||
**Risk**: a future dependency upgrade introducing a Critical/High CVE could ship to `dev-arm` undetected.
|
||||
|
||||
**Remediation**: insert a step before `build-push`:
|
||||
|
||||
```yaml
|
||||
- name: dep-audit
|
||||
image: oven/bun:1.3.11-alpine
|
||||
commands:
|
||||
- bun audit --severity high # exits non-zero if any High/Critical
|
||||
```
|
||||
|
||||
**Severity**: MEDIUM (visible CVE exposure, easy to fix).
|
||||
|
||||
### F-INF-3 — No vulnerability scan on the produced image — MEDIUM
|
||||
|
||||
**Evidence**: `.woodpecker/build-arm.yml` does not invoke Trivy, Grype, or any image scanner. Base-image CVEs in `nginx:alpine` are invisible to CI.
|
||||
|
||||
**Risk**: nginx alpine releases ship with periodic CVEs (latest run-time vulns in the OS packages). Without a scan, the image can ship vulnerable.
|
||||
|
||||
**Remediation**:
|
||||
|
||||
```yaml
|
||||
- name: image-scan
|
||||
image: aquasec/trivy:latest
|
||||
commands:
|
||||
- trivy image --severity HIGH,CRITICAL --exit-code 1 \
|
||||
$REGISTRY_HOST/azaion/ui:$TAG
|
||||
```
|
||||
|
||||
**Severity**: MEDIUM.
|
||||
|
||||
### F-INF-4 — No SBOM emission and no image signing — MEDIUM
|
||||
|
||||
**Evidence**: pipeline produces and pushes images but does not emit an SBOM (Syft/cyclonedx) and does not sign images (cosign).
|
||||
|
||||
**Risk**: registry compromise or MITM during pull cannot be detected. Post-deploy SBOM-based vulnerability triage is impossible.
|
||||
|
||||
**Remediation**: best owned at the suite level — coordinate with the registry team. Adding cosign requires a key management decision (KMS vs. file-based). Typical ordering:
|
||||
1. Add `syft packages docker:$image -o cyclonedx-json > sbom.json` — emit and store SBOM as a build artifact.
|
||||
2. Configure cosign keyless via OIDC (if Woodpecker integrates) OR file-based key from secrets.
|
||||
3. `cosign sign --key cosign.key $image` step + `cosign verify` step in the deploy pipeline.
|
||||
|
||||
**Severity**: MEDIUM (supply-chain integrity).
|
||||
|
||||
---
|
||||
|
||||
## Network Security & Headers
|
||||
|
||||
### `nginx.conf`
|
||||
|
||||
**Verified controls**:
|
||||
- Strict `proxy_pass` to fixed upstream service names (no user-controlled URL routing).
|
||||
- `client_max_body_size 500M` — bounded.
|
||||
- SPA fallback `try_files $uri $uri/ /index.html` — safe (no upstream rewrite).
|
||||
- `proxy_read_timeout 86400` on `/api/annotations/` (SSE) and `600` on `/api/detect/` (long video) — explicit per-route limits, not a global config.
|
||||
|
||||
**Findings**:
|
||||
|
||||
### F-INF-2 — Missing security response headers — MEDIUM
|
||||
|
||||
**Evidence**: zero `add_header` directives in `nginx.conf`. None of the standard hardening headers are emitted to the browser:
|
||||
- `Content-Security-Policy`
|
||||
- `X-Frame-Options` / CSP `frame-ancestors`
|
||||
- `Strict-Transport-Security` (depends on suite ingress decision — coordinate)
|
||||
- `Referrer-Policy: strict-origin-when-cross-origin`
|
||||
- `X-Content-Type-Options: nosniff`
|
||||
|
||||
**Bearer-redaction**: SSE URLs include `?access_token=…` → currently logged in plaintext to nginx access logs. No redaction directive.
|
||||
|
||||
**Risk**:
|
||||
- Without CSP, any future XSS (we have none today, but the surface evolves) gets unrestricted execution.
|
||||
- Without `frame-ancestors`/`X-Frame-Options`, the SPA can be framed → clickjacking on the operator's session.
|
||||
- Without `Referrer-Policy`, internal SPA URLs leak to external sites if the operator clicks an outbound link.
|
||||
- Bearers persist in nginx access logs (operator-internal but still — log retention compounds).
|
||||
|
||||
**Remediation** (one PR; recommended starting point per `_docs/00_problem/security_approach.md` §9):
|
||||
|
||||
```nginx
|
||||
add_header Content-Security-Policy "default-src 'self'; img-src 'self' https: data:; connect-src 'self' https://api.openweathermap.org/; frame-ancestors 'none'; object-src 'none'" always;
|
||||
add_header X-Frame-Options "DENY" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
# add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always; # decide with suite ingress
|
||||
|
||||
# In the SSE location (e.g. /api/annotations/):
|
||||
log_format azaion_redact '$remote_addr - $remote_user [$time_local] '
|
||||
'"$request_method $uri ' # drop ?args from the access log
|
||||
'$server_protocol" $status $body_bytes_sent';
|
||||
access_log /var/log/nginx/access.log azaion_redact;
|
||||
```
|
||||
|
||||
**Cycle 2 specific**: the new `<TileLayer crossOrigin="use-credentials">` (AZ-498) needs the production env to point at the same-origin nginx path. The CSP `connect-src 'self'` above already permits this; if the suite-internal `satellite-provider` lives on a different origin it must be added explicitly. This is captured in the AZ-498 deploy gate (Step 16).
|
||||
|
||||
**Severity**: MEDIUM.
|
||||
|
||||
---
|
||||
|
||||
## Environment Configuration
|
||||
|
||||
### `.env.example` files
|
||||
|
||||
| File | Status |
|
||||
|------|--------|
|
||||
| `.env.example` (ui/) | Clean — only documentation comments and empty/placeholder values. Cycle 2 added `VITE_OWM_API_KEY=<your-openweathermap-api-key>`, `VITE_OWM_BASE_URL=`, `VITE_SATELLITE_TILE_URL=` placeholders. |
|
||||
| `mission-planner/.env.example` | Clean — same pattern. Includes `VITE_SATELLITE_TILE_URL=https://server.arcgisonline.com/...` (legacy default; does NOT contain a real auth key). |
|
||||
|
||||
**Verified**: no real secrets committed to either `.env.example`. Both use the `<your-...>` convention.
|
||||
|
||||
### `.gitignore`
|
||||
|
||||
**Verified** (`grep` against root `.gitignore`): excludes `.env.local`, `.env.development.local`, `.env.test.local`, `.env.production.local`. Real secrets are properly kept out of git.
|
||||
|
||||
**Recommendation**: add `mission-planner/.env.local` and equivalents to `mission-planner/.gitignore` (or a root-level pattern that catches both roots) for symmetry. Verify by grep — not part of this audit's automated checks.
|
||||
|
||||
---
|
||||
|
||||
## E2E Harness Security
|
||||
|
||||
### `e2e/docker-compose.suite-e2e.yml`
|
||||
|
||||
**Verified controls**:
|
||||
- Isolated `azaion-test-net` bridge network — no host network access for the runner.
|
||||
- Stubbed external endpoints (`owm-stub`, `tile-stub`) — Playwright tests cannot accidentally hit production OWM or external tile providers.
|
||||
- Test DB password is `azaion`/`azaion` — visible in plaintext, but acceptable: the DB is bound to the isolated network and lives only for the e2e run.
|
||||
- `ENABLE_TEST_ONLY_ENDPOINTS: "true"` is gated to the e2e profile.
|
||||
- The `azaion-ui` image build wires `VITE_SATELLITE_TILE_URL` to the in-cluster `tile-stub` — confirming AZ-498's env-driven design works end-to-end with no real-world tile auth required.
|
||||
|
||||
**No findings.**
|
||||
|
||||
---
|
||||
|
||||
## Cycle-2 specific infrastructure review (AZ-498, AZ-499)
|
||||
|
||||
| Change | Infra review |
|
||||
|--------|--------------|
|
||||
| `VITE_SATELLITE_TILE_URL` introduction | OK. `.env.example` documents prod requirement (same-origin nginx path) explicitly. E2E compose wires the test value. No infra regression. |
|
||||
| Cookie-credentialed tile fetch (`crossOrigin="use-credentials"`) | OK conditional on prod env override. The default (`http://localhost:5100/...`) only works in local dev; misconfiguration in stage/prod (forgetting to set the env var) results in tile failure (UX impact, no security regression). The Step 16 deploy gate covers this. |
|
||||
| `STC-SEC1C` static check (OWM key in `mission-planner/`) | OK — added to `scripts/run-tests.sh`. No CI integration today (see F-INF-1) — same gap that affects everything else. |
|
||||
| `mission-planner/.env.example` updated | OK — placeholder convention preserved, no real secrets. |
|
||||
|
||||
---
|
||||
|
||||
## Recommendations roll-up
|
||||
|
||||
| ID | Severity | Effort | Owner | Recommendation |
|
||||
|----|----------|--------|-------|----------------|
|
||||
| F-INF-1 | MEDIUM | 1 SP | UI | Add `bun audit --severity high` step to `.woodpecker/build-arm.yml` |
|
||||
| F-INF-2 | MEDIUM | 2 SP | UI | Add CSP / X-Frame-Options / Referrer-Policy / X-Content-Type-Options + bearer-redaction log format to `nginx.conf` |
|
||||
| F-INF-3 | MEDIUM | 2 SP | UI / DevOps | Add Trivy image scan step to `.woodpecker/build-arm.yml` |
|
||||
| F-INF-4 | MEDIUM | 3-5 SP | Suite-wide | SBOM + cosign — coordinate registry decision suite-wide |
|
||||
| F-INF-5 | LOW | 1 SP | UI | Switch to `nginxinc/nginx-unprivileged` and add `HEALTHCHECK` directive |
|
||||
|
||||
---
|
||||
|
||||
## Self-verification
|
||||
|
||||
- [x] `Dockerfile` reviewed
|
||||
- [x] `.woodpecker/build-arm.yml` reviewed
|
||||
- [x] `nginx.conf` reviewed
|
||||
- [x] `e2e/docker-compose.suite-e2e.yml` reviewed
|
||||
- [x] `.env.example` files reviewed (root + `mission-planner/`)
|
||||
- [x] `.gitignore` reviewed
|
||||
- [x] Cycle 2 deltas individually reviewed
|
||||
@@ -0,0 +1,184 @@
|
||||
# OWASP Top 10 Review — Azaion UI
|
||||
|
||||
**Date**: 2026-05-12
|
||||
**Framework**: [OWASP Top 10 — 2021](https://owasp.org/www-project-top-ten/) (current edition; 2024 revision is in draft and not yet final at time of audit)
|
||||
**Scope**: Browser SPA (`src/`) + nginx + supporting infrastructure
|
||||
**Cycle**: Phase B / Cycle 2
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| # | Category | Status | Notes |
|
||||
|---|----------|--------|-------|
|
||||
| A01 | Broken Access Control | PASS_WITH_KNOWN | Server-authoritative; 1 known UX-only client gap (`/admin` route) |
|
||||
| A02 | Cryptographic Failures | PASS_WITH_KNOWN | Bearer in memory; refresh in HttpOnly cookie; 1 accepted trade-off (SSE bearer-in-query) |
|
||||
| A03 | Injection | PASS | No eval/Function; React JSX escapes; URL params encoded |
|
||||
| A04 | Insecure Design | PASS | Same-origin nginx + bearer-header + SameSite=Strict cookie pattern |
|
||||
| A05 | Security Misconfiguration | FAIL | nginx missing CSP, X-Frame-Options, HSTS, Referrer-Policy, X-Content-Type-Options, log redaction |
|
||||
| A06 | Vulnerable & Outdated Components | FAIL | 1 High vite (dev-server only) + 2 Moderate (vite, postcss) — see `dependency_scan.md` |
|
||||
| A07 | Identification & Authentication Failures | PASS_WITH_KNOWN | 1 known cold-load refresh bug (F2 in `security_approach.md`) |
|
||||
| A08 | Software & Data Integrity Failures | FAIL | No SBOM, no image signing, no `bun audit` in CI |
|
||||
| A09 | Security Logging & Monitoring Failures | N/A | Server-side concern; SPA is operator-internal with no client telemetry |
|
||||
| A10 | Server-Side Request Forgery | N/A | Browser SPA has no server-side request surface |
|
||||
|
||||
**Overall posture**: PASS_WITH_WARNINGS. No exploitable vulnerabilities in the production browser bundle. Multiple infrastructure-level hardening gaps are tracked at the suite level (nginx headers, CI scanning).
|
||||
|
||||
---
|
||||
|
||||
## A01 — Broken Access Control — PASS_WITH_KNOWN
|
||||
|
||||
**Server-authoritative model.** Per `_docs/00_problem/security_approach.md` §2 and `_docs/02_document/architecture.md` §7: every authenticated endpoint validates `User.role` and `permissions[]` server-side. The browser is treated as untrusted; the UI inspects `AuthUser.role` only to render or hide nav elements.
|
||||
|
||||
**Verified controls**:
|
||||
- The 401-recovery path in `src/api/client.ts:90` attempts a server-side refresh and surfaces 401/403 to the user — there is no path that "promotes" a denied request client-side.
|
||||
- IDOR / horizontal-escalation surface: every API URL embeds the suite-side resource ID; the UI does not assemble paths from user input that could be substituted to access another tenant's data. The server is the gate.
|
||||
- CORS misconfiguration: same-origin via nginx (`nginx.conf:6-72`) — no `Access-Control-Allow-Origin: *` headers anywhere in nginx config.
|
||||
- Directory traversal: nginx serves `dist/` only with the SPA fallback `try_files $uri $uri/ /index.html` (`nginx.conf:73-75`); proxy_pass directives are scoped to fixed upstream prefixes.
|
||||
|
||||
**Known gap (UX-only, not exploitable)**:
|
||||
- `/admin` route lacks a client-side role-gate. Non-admin users navigating to `/admin` see the broken admin UI flicker before server-side 403s reject the API calls. Tracked as **finding F2 / AC-22** in `security_approach.md`. Server is authoritative — no privilege escalation. Step 4 hardening was paused; remains a Phase B candidate.
|
||||
|
||||
---
|
||||
|
||||
## A02 — Cryptographic Failures — PASS_WITH_KNOWN
|
||||
|
||||
**Verified controls** (per `security_approach.md` §3 + Cycle 2 audit):
|
||||
- Bearer JWT held only in `AuthContext` React state (memory). Verified by manual grep + `STC-SEC3` static check + `NFT-SEC-01` test (`src/auth/AuthContext.test.tsx`).
|
||||
- Refresh token in `Secure HttpOnly SameSite=Strict` cookie — never readable by JS. Verified by `STC-SEC4` static check.
|
||||
- TLS termination at suite ingress (out-of-band of this workspace; documented in `security_approach.md` §9).
|
||||
- No symmetric encryption performed client-side (no banned crypto libs per `tests/security/banned-deps.json` `signature_libs`).
|
||||
- No localStorage/sessionStorage/IndexedDB persistence of secrets (`O2` anti-criterion, `persistence_libs` deny-list).
|
||||
|
||||
**Accepted trade-off (ADR-008)**:
|
||||
- SSE bearer-in-query-string (`src/api/sse.ts:11`). `EventSource` cannot send headers; the bearer rides in `?access_token=…`. HTTPS encrypts the URL on the wire, but it appears in nginx access logs and (low risk) browser history. Mitigation: log redaction at the nginx layer is **NOT yet configured** (tracked under A05 below). Acknowledged in `security_approach.md` §4.
|
||||
|
||||
**Cycle 2 cryptography review**:
|
||||
- `<TileLayer crossOrigin="use-credentials">` (AZ-498) — sends cookies on tile requests when same-origin. **Does NOT** send the bearer (bearer travels via `Authorization` header, which Leaflet does not set). Cookie is `SameSite=Strict`, so cross-site is impossible. Clean.
|
||||
|
||||
---
|
||||
|
||||
## A03 — Injection — PASS
|
||||
|
||||
**Browser-side surfaces**:
|
||||
- **No SQL/NoSQL** — the SPA never builds SQL queries; all DB access is server-side.
|
||||
- **No eval / Function constructor / setTimeout-of-string** — verified by manual grep this audit (zero matches in `src/` or `mission-planner/`).
|
||||
- **No template injection** — React JSX escapes string children by default; no `dangerouslySetInnerHTML` anywhere (verified this audit + grep).
|
||||
- **URL parameter construction** — searched `src/` for hand-built query strings; all observed cases use `encodeURIComponent` or template literals over typed values (e.g. `src/api/sse.ts:11`).
|
||||
- **OS command injection** — N/A (browser has no shell).
|
||||
|
||||
**Output encoding** (XSS):
|
||||
- React 19 default escaping handles all string content.
|
||||
- The `HelpModal` ships hardcoded English strings inline (P6 violation — i18n only; XSS-safe).
|
||||
- Annotation download tainted-canvas issue (`AnnotationsPage.handleDownload`) is a UX bug, not a security defect — image data may taint the canvas, the download silently fails. Already documented in `security_approach.md` §8.
|
||||
|
||||
**Cycle 2 review**: `getTileUrl()` reads `import.meta.env.VITE_SATELLITE_TILE_URL` (a build-time-frozen string) and passes it to Leaflet's `TileLayer.url` template. There is no user-controlled input on this path — no template-injection or URL-injection surface.
|
||||
|
||||
---
|
||||
|
||||
## A04 — Insecure Design — PASS
|
||||
|
||||
**Architectural pattern**:
|
||||
- Same-origin via nginx → cookies scope cleanly; no cross-origin CSRF surface.
|
||||
- Bearer in `Authorization` header (cannot be auto-attached by a cross-origin form).
|
||||
- `SameSite=Strict` refresh cookie — no CSRF on the refresh endpoint either.
|
||||
- Bearer-in-memory + short TTL + 401-retry → minimises window of compromise after XSS.
|
||||
- No client-side persistence of mutable state — server is source of truth.
|
||||
|
||||
This pattern is the recommended approach for an internal-operator SPA per current OWASP cheatsheet guidance.
|
||||
|
||||
**No design-level violations identified.** The only design decision flagged in past reviews — the SSE bearer-in-query-string — is an `EventSource`-protocol limitation, not a design choice.
|
||||
|
||||
---
|
||||
|
||||
## A05 — Security Misconfiguration — FAIL
|
||||
|
||||
**Failures (confirmed by `nginx.conf` inspection)**:
|
||||
- **NO** `Content-Security-Policy` header (recommended starting point in `security_approach.md` §9).
|
||||
- **NO** `X-Frame-Options: DENY` (or CSP `frame-ancestors`). Clickjacking surface.
|
||||
- **NO** `Referrer-Policy: strict-origin-when-cross-origin`.
|
||||
- **NO** `Strict-Transport-Security` (TLS terminated at suite ingress; HSTS should be set there or here — needs decision).
|
||||
- **NO** `X-Content-Type-Options: nosniff`.
|
||||
- **NO** bearer-redaction in nginx access logs for SSE URLs (acknowledged in `security_approach.md` §4 and §9).
|
||||
|
||||
**Other**:
|
||||
- Default credentials: not applicable — auth is server-side via the `admin/` service.
|
||||
- Unnecessary features enabled: nginx config is minimal (only `client_max_body_size 500M` + per-service proxy_pass).
|
||||
- Verbose error messages in production: not applicable — Vite production build strips dev banners.
|
||||
|
||||
**Remediation**: track all six header/redaction items as a single Phase B ticket against the SPA's `nginx.conf` (low risk, code-only change). See `infrastructure_review.md` F-INF-2.
|
||||
|
||||
---
|
||||
|
||||
## A06 — Vulnerable & Outdated Components — FAIL
|
||||
|
||||
**Findings** (full detail in `dependency_scan.md`):
|
||||
- F-DEP-1: vite `<= 6.4.1` — Arbitrary File Read via Dev Server WebSocket — HIGH (dev-server only).
|
||||
- F-DEP-2: vite `<= 6.4.1` — Path Traversal in Optimized Deps `.map` — MODERATE (dev-server only).
|
||||
- F-DEP-3: postcss `< 8.5.10` — XSS via Unescaped `</style>` — MODERATE (low surface — no untrusted CSS in this build).
|
||||
|
||||
**Production-bundle exploitability**: NONE — all three findings are dev-time only. Production runtime is `nginx:alpine` serving pre-built static assets.
|
||||
|
||||
**Verdict**: FAIL on the audit category because `bun audit` reports a HIGH advisory and the remediation is trivially available (`bun update vite`). Lifting these immediately is straightforward.
|
||||
|
||||
---
|
||||
|
||||
## A07 — Identification & Authentication Failures — PASS_WITH_KNOWN
|
||||
|
||||
**Verified controls**:
|
||||
- Bearer JWT signed and validated server-side; UI never inspects token contents.
|
||||
- Refresh-token rotation on 401 (`src/api/client.ts:88-99`).
|
||||
- Server is authoritative on lockout, brute-force, and MFA enforcement.
|
||||
- Logout: `POST /api/admin/auth/logout` clears bearer in memory; server invalidates the refresh cookie.
|
||||
|
||||
**Known gap**:
|
||||
- Bootstrap (cold-load) refresh missing `credentials:'include'` (`src/auth/AuthContext.tsx:24`). Effect: even with a valid refresh cookie, cold-load refresh fails → user is bounced to `/login`. Tracked as **F2 / AC-01** with a `it.fails` quarantined test that flips when the fix lands. Documented in `security_approach.md` §1. Functional/UX bug, not a security regression — server still rejects unauthenticated requests properly.
|
||||
|
||||
---
|
||||
|
||||
## A08 — Software & Data Integrity Failures — FAIL
|
||||
|
||||
**Verified controls**:
|
||||
- `bun install --frozen-lockfile` in `Dockerfile:4` enforces lockfile fidelity (no in-build dependency drift).
|
||||
- `AZAION_REVISION=$CI_COMMIT_SHA` baked into the image (`Dockerfile:9-10`); OCI labels stamped at push time (`.woodpecker/build-arm.yml:23-28`).
|
||||
|
||||
**Failures**:
|
||||
- **NO SBOM emission** (Syft / cyclonedx-bom). Cannot audit the produced image's bill of materials post-hoc.
|
||||
- **NO image signing** (cosign / docker content trust). The registry has no integrity guarantee on `ui:dev-arm` / `ui:stage-arm` / `ui:main-arm`.
|
||||
- **NO vulnerability scan** (Trivy / Grype) on the produced image. Base-image CVEs (e.g. in `nginx:alpine`) are invisible to CI.
|
||||
- **NO `bun audit` step** in `.woodpecker/build-arm.yml` — Cycle 2 dependency findings would not have failed CI.
|
||||
|
||||
**Remediation priorities**:
|
||||
1. Quick: add `bun audit --high` exit-code gate to the pipeline (catches future regressions).
|
||||
2. Medium: add Trivy scan on the produced image (surfaces base-image and OS-package CVEs).
|
||||
3. Long: SBOM + cosign signing — coordinate at the suite level (depends on registry capabilities).
|
||||
|
||||
See `infrastructure_review.md` F-INF-1, F-INF-3, F-INF-4.
|
||||
|
||||
---
|
||||
|
||||
## A09 — Security Logging & Monitoring Failures — N/A
|
||||
|
||||
The SPA does not emit audit logs. All audit events are emitted by the server-side suite services (`admin/`, `flights/`, `annotations/`, `detect/`, `loader/`, `resource/`, `gps-denied-*`, `autopilot/`). The browser console is the only client-side log surface; no centralized client telemetry exists today.
|
||||
|
||||
**Justification for N/A** (per `security_approach.md` §10 + `_docs/00_problem/anti_criteria.md`): the SPA is internal/operator-only — observability is a suite-level concern intentionally NOT duplicated client-side.
|
||||
|
||||
---
|
||||
|
||||
## A10 — Server-Side Request Forgery — N/A
|
||||
|
||||
The codebase under audit is a browser SPA. There is no server-side request surface that accepts URLs from user input. The SPA's outbound calls are:
|
||||
- Same-origin nginx proxies (`/api/<service>/*`) — fixed paths, server-authoritative routing.
|
||||
- Build-time-fixed env URLs: `VITE_OWM_BASE_URL` (defaults to `https://api.openweathermap.org/data/2.5`), `VITE_SATELLITE_TILE_URL` (defaults to `http://localhost:5100/...`).
|
||||
|
||||
Neither URL is user-controllable at runtime. The only browser endpoint resembling SSRF — passing the user's address through to Google Geocode in `mission-planner/` — is in the port-source which is NOT shipped (see `static_analysis.md` F-SAST-1). The suite-level recommendation is to proxy any future geocoding through a server-side endpoint to remove the client-visible API key, which would naturally introduce real SSRF surface; that future ticket should explicitly validate URL inputs.
|
||||
|
||||
---
|
||||
|
||||
## Self-verification
|
||||
|
||||
- [x] All ten OWASP Top 10 (2021) categories assessed
|
||||
- [x] Every FAIL has at least one specific finding with file path / line
|
||||
- [x] N/A categories have explicit justification
|
||||
- [x] `security_approach.md` cross-referenced — every existing known-gap is reflected here, not hidden
|
||||
- [x] Cycle 2 changes (AZ-498, AZ-499) reviewed under each applicable category
|
||||
@@ -0,0 +1,154 @@
|
||||
# Security Audit Report — Azaion UI
|
||||
|
||||
**Date**: 2026-05-12
|
||||
**Scope**: `src/` (production SPA), `mission-planner/src/` (port-source — in git history but NOT in production bundle), `nginx.conf`, `Dockerfile`, `.woodpecker/build-arm.yml`, `e2e/` harness, `.env.example` files
|
||||
**Cycle**: Phase B / Cycle 2 (post AZ-498, AZ-499)
|
||||
**Verdict**: **FAIL** — 1 HIGH-severity secret leak in port-source (`F-SAST-1` Google Geocode API key) plus 1 HIGH-severity dependency advisory (`F-DEP-1` vite — dev-server only, no prod exposure)
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Severity | Count | Notes |
|
||||
|----------|-------|-------|
|
||||
| Critical | 0 | — |
|
||||
| High | 2 | F-SAST-1 (production-bundle exposure: NONE today; git-history exposure: HIGH); F-DEP-1 (production exposure: NONE; dev-server: HIGH) |
|
||||
| Medium | 7 | F-SAST-2, F-SAST-3, F-DEP-2, F-DEP-3, F-INF-1, F-INF-2, F-INF-3, F-INF-4 |
|
||||
| Low | 2 | F-SAST-4, F-INF-5 |
|
||||
|
||||
**Production browser bundle is clean** — no exploitable findings. All HIGH-severity items are concentrated in (a) port-source code that does not ship and (b) dev-time tooling (Vite dev server). The audit's FAIL verdict reflects:
|
||||
1. The port-source key is a real secret in real git history → must be revoked + externalized following the AZ-499 pattern.
|
||||
2. CI does not run `bun audit`, so the High Vite advisory shipped through Cycle 2 unflagged → procedural gap to close.
|
||||
|
||||
## OWASP Top 10 (2021) Assessment
|
||||
|
||||
| # | Category | Status | Findings |
|
||||
|---|----------|--------|----------|
|
||||
| A01 | Broken Access Control | PASS_WITH_KNOWN | 1 known UX gap (`/admin` route, F2/AC-22 — pre-existing) |
|
||||
| A02 | Cryptographic Failures | PASS_WITH_KNOWN | 1 accepted trade-off (SSE bearer-in-query, ADR-008) |
|
||||
| A03 | Injection | PASS | — |
|
||||
| A04 | Insecure Design | PASS | — |
|
||||
| A05 | Security Misconfiguration | FAIL | F-INF-2 (nginx headers + log redaction missing) |
|
||||
| A06 | Vulnerable & Outdated Components | FAIL | F-DEP-1, F-DEP-2, F-DEP-3 |
|
||||
| A07 | Identification & Authentication Failures | PASS_WITH_KNOWN | 1 known cold-load refresh bug (F2 — pre-existing) |
|
||||
| A08 | Software & Data Integrity Failures | FAIL | F-INF-1, F-INF-3, F-INF-4 |
|
||||
| A09 | Security Logging & Monitoring Failures | N/A | Server-side concern (operator-internal SPA) |
|
||||
| A10 | Server-Side Request Forgery | N/A | Browser SPA has no server-side request surface |
|
||||
|
||||
## Findings (severity-ranked)
|
||||
|
||||
| # | Severity | Category | Location | Title |
|
||||
|---|----------|----------|----------|-------|
|
||||
| F-SAST-1 | **HIGH** | Secrets in code | `mission-planner/src/config.ts:2` | Hardcoded Google Geocode API key in port-source |
|
||||
| F-DEP-1 | **HIGH** | Vulnerable component | `vite@6.4.1` (both roots) | Vite Arbitrary File Read via Dev Server WebSocket (GHSA-p9ff-h696-f583) — dev-server only |
|
||||
| F-INF-1 | MEDIUM | CI/CD | `.woodpecker/build-arm.yml` | `bun audit` not gated in CI pipeline |
|
||||
| F-INF-2 | MEDIUM | Misconfiguration | `nginx.conf` | Missing CSP, X-Frame-Options, HSTS, Referrer-Policy, X-Content-Type-Options, log-redaction |
|
||||
| F-INF-3 | MEDIUM | Supply chain | `.woodpecker/build-arm.yml` | No image vulnerability scan (Trivy/Grype) |
|
||||
| F-INF-4 | MEDIUM | Supply chain | `.woodpecker/build-arm.yml` | No SBOM emission, no image signing (cosign) |
|
||||
| F-DEP-2 | MEDIUM | Vulnerable component | `vite@6.4.1` | Vite Path Traversal in Optimized Deps `.map` (GHSA-4w7w-66w2-5vf9) — dev-server only |
|
||||
| F-DEP-3 | MEDIUM | Vulnerable component | `postcss@8.5.8` (transitive) | PostCSS XSS via Unescaped `</style>` (GHSA-qx2v-qp2m-jg93) — low surface |
|
||||
| F-SAST-2 | MEDIUM | Supply chain | `mission-planner/src/icons/PointIcons.tsx:7` | `unpkg.com` CDN reference in port-source |
|
||||
| F-SAST-3 | MEDIUM | Coverage gap | `scripts/run-tests.sh` (`STC-SEC2`) | No-CDN gate does not scan `mission-planner/` |
|
||||
| F-SAST-4 | LOW | Future risk | `mission-planner/src/constants/tileUrls.ts:2-3` | Port-source still uses third-party tile fallbacks |
|
||||
| F-INF-5 | LOW | Container hardening | `Dockerfile` | nginx runs as root master process; no `HEALTHCHECK` directive |
|
||||
|
||||
### Finding Details
|
||||
|
||||
#### F-SAST-1 — Hardcoded Google Geocode API key — HIGH
|
||||
|
||||
- **Location**: `mission-planner/src/config.ts:2`
|
||||
- **Value**: `AIzaSyAhvDeYukuyWVrQYbRhuv91bsi_jj5_Iys`
|
||||
- **Description**: The Google Geocode API key is committed in `mission-planner/` (port-source). Used by `mission-planner/src/flightPlanning/LeftBoard.tsx:114` for address-to-coords lookups.
|
||||
- **Production-bundle exposure**: NONE today. `src/` does not import from `mission-planner/`; `Dockerfile` builds only `src/`-rooted Vite. The key is NOT in `dist/`.
|
||||
- **Git-history exposure**: HIGH. Anyone with repo read access can extract the key. Same threat class as the OWM key resolved by AZ-499.
|
||||
- **Impact**: Quota theft, billing-account abuse, accelerated risk if `mission-planner/` is later ported into the SPA without remediation.
|
||||
- **Remediation** (mirror AZ-499 / AC-42 pattern):
|
||||
1. **Revoke** the key at https://console.cloud.google.com/google/maps-apis/credentials (manual, OUT-OF-BAND, USER ACTION). Capture evidence.
|
||||
2. Externalize: `import.meta.env.VITE_GOOGLE_GEOCODE_KEY` in `mission-planner/src/config.ts` with fail-soft if unset.
|
||||
3. Update `mission-planner/.env.example` with placeholder.
|
||||
4. Extend `tests/security/banned-deps.json` `owm_key_in_source` (or add a sibling `google_key_in_source`) section to also block the literal Google key.
|
||||
5. Long-term: route geocoding via suite-side proxy when the SPA needs it.
|
||||
- See: `static_analysis.md` F-SAST-1.
|
||||
|
||||
#### F-DEP-1 — Vite Arbitrary File Read via Dev Server WebSocket — HIGH
|
||||
|
||||
- **Location**: `vite@6.4.1` (resolved in `bun.lock`, both `ui/` and `mission-planner/` roots)
|
||||
- **Advisory**: [GHSA-p9ff-h696-f583](https://github.com/advisories/GHSA-p9ff-h696-f583)
|
||||
- **Description**: WebSocket endpoint exposed by `vite dev` allows arbitrary local-file read via path traversal.
|
||||
- **Production-bundle exposure**: NONE. The Vite dev server is never present in production (`Dockerfile` final stage is `nginx:alpine` serving static `dist/`).
|
||||
- **Developer-machine exposure**: HIGH if `bun run dev --host` is ever used (binding to `0.0.0.0`); MODERATE for the default `localhost` binding (still a browser-side script attack vector via DNS rebinding).
|
||||
- **Remediation**: `bun update vite` in both roots → `vite >= 6.4.2`. Verify build + fast tests still pass.
|
||||
- See: `dependency_scan.md` F-DEP-1.
|
||||
|
||||
(Full detail for F-INF-1 .. F-INF-5 in `infrastructure_review.md`; for F-DEP-2/F-DEP-3 in `dependency_scan.md`; for F-SAST-2/F-SAST-3/F-SAST-4 in `static_analysis.md`. Not duplicated here.)
|
||||
|
||||
---
|
||||
|
||||
## Dependency Vulnerabilities
|
||||
|
||||
| Package | GHSA / Advisory | Severity | Installed | Fix |
|
||||
|---------|-----------------|----------|-----------|-----|
|
||||
| `vite` | [GHSA-p9ff-h696-f583](https://github.com/advisories/GHSA-p9ff-h696-f583) | HIGH | 6.4.1 | `>= 6.4.2` (bun update vite) |
|
||||
| `vite` | [GHSA-4w7w-66w2-5vf9](https://github.com/advisories/GHSA-4w7w-66w2-5vf9) | MODERATE | 6.4.1 | `>= 6.4.2` (same upgrade) |
|
||||
| `postcss` | [GHSA-qx2v-qp2m-jg93](https://github.com/advisories/GHSA-qx2v-qp2m-jg93) | MODERATE | 8.5.8 | `>= 8.5.10` (transitive — flows through Vite upgrade) |
|
||||
|
||||
A single `bun update vite` in each root fixes all three.
|
||||
|
||||
---
|
||||
|
||||
## Recommendations
|
||||
|
||||
### Immediate (HIGH — block deploys until done)
|
||||
|
||||
- [ ] **F-SAST-1 (USER ACTION + CODE)**: Revoke the Google Geocode API key at the Google Cloud Console, then externalize per AZ-499 pattern. Mirror the manual evidence-capture protocol used for AZ-499 AC-7. Recommended ticket: `AZ-NEW — Externalize Google Geocode key in mission-planner port-source` (3 SP — same shape as AZ-499 minus AC-8 misattribution).
|
||||
- [ ] **F-DEP-1 / F-DEP-2 / F-DEP-3 (CODE)**: `bun update vite` in `ui/` and `mission-planner/`. Re-run `bun audit` to confirm zero findings. Recommended ticket: `AZ-NEW — Update Vite to fix CVE-2026 advisories` (1 SP).
|
||||
|
||||
### Short-term (MEDIUM — Phase B)
|
||||
|
||||
- [ ] **F-INF-1**: Add `bun audit --severity high` step to `.woodpecker/build-arm.yml` so future advisory regressions fail CI (1 SP).
|
||||
- [ ] **F-INF-2**: Add CSP, X-Frame-Options, Referrer-Policy, X-Content-Type-Options + bearer-redaction log format to `nginx.conf` (2 SP). Coordinate HSTS decision with suite ingress.
|
||||
- [ ] **F-INF-3**: Add Trivy image-scan step to `.woodpecker/build-arm.yml` after `docker build` (2 SP).
|
||||
- [ ] **F-SAST-2**: Bundle Leaflet marker icon locally instead of `unpkg.com` CDN reference (covered by the same port-source cleanup as F-SAST-1).
|
||||
- [ ] **F-SAST-3**: Widen no-CDN static gate to scan `mission-planner/` — move pattern into `tests/security/banned-deps.json` and use the existing `check-banned-deps.mjs` widening (2 SP).
|
||||
|
||||
### Long-term (Suite-wide / Hardening)
|
||||
|
||||
- [ ] **F-INF-4**: SBOM (Syft/cyclonedx) + cosign image signing — coordinate registry capability with suite team (3-5 SP).
|
||||
- [ ] **F-SAST-4**: Mission-planner port-source modernization will resolve the third-party tile fallbacks naturally — no separate ticket needed.
|
||||
- [ ] **F-INF-5**: `nginxinc/nginx-unprivileged` migration + `HEALTHCHECK` directive (1 SP, low priority).
|
||||
|
||||
### Pre-existing (not introduced by this audit; tracked elsewhere)
|
||||
|
||||
- F2 / AC-01 — bootstrap refresh missing `credentials:'include'` (`src/auth/AuthContext.tsx:24`). Quarantined-test acknowledged. Phase B fix.
|
||||
- AC-22 — `/admin` route lacks client-side role-gate. Server-authoritative, no exploit. Phase B UX fix.
|
||||
- ADR-008 — SSE bearer-in-query-string. Accepted trade-off; mitigation lives in F-INF-2 (nginx log redaction).
|
||||
- AZ-499 AC-7 — OWM key revocation manual deliverable. **Pending USER action.**
|
||||
|
||||
---
|
||||
|
||||
## Cycle 2 — security regression check
|
||||
|
||||
No security regressions introduced by AZ-498 or AZ-499. Both changes pass static + fast test suites; the cookie-credentialed tile fetch is correctly scoped to `SameSite=Strict` and same-origin; the OWM env hardening closes the previously quarantined `NFT-SEC-09` source check.
|
||||
|
||||
`STC-SEC1C` is now part of the static gate and would catch any future re-introduction of the literal OWM key in either `src/` or `mission-planner/`.
|
||||
|
||||
---
|
||||
|
||||
## Verdict justification
|
||||
|
||||
The verdict is **FAIL** because:
|
||||
1. F-SAST-1 is a real third-party API key in real git history. The same finding class as AZ-499 — same remediation pattern, same urgency, same need for out-of-band revocation.
|
||||
2. F-DEP-1 is a HIGH advisory against a current direct dependency. Even with no production exposure, OWASP A06 categorically fails on any actionable HIGH advisory.
|
||||
|
||||
Both findings have one-line remediations. Once F-SAST-1 is revoked + externalized and F-DEP-1 is upgraded, a follow-up audit cycle should re-rate the verdict to PASS_WITH_WARNINGS pending the MEDIUM infrastructure tickets.
|
||||
|
||||
The production browser bundle itself is **not vulnerable** — the SPA is well-architected (server-authoritative auth, bearer-in-memory + HttpOnly cookie, no eval/injection surface, no client-side persistence). The deficiencies are at the supply-chain, infrastructure, and port-source layers.
|
||||
|
||||
## Self-verification
|
||||
|
||||
- [x] All findings from Phases 1–4 included
|
||||
- [x] No duplicate findings (cross-references used instead)
|
||||
- [x] Every finding has remediation guidance
|
||||
- [x] Verdict matches severity logic (FAIL on any HIGH)
|
||||
- [x] Production-vs-dev impact distinguished for each HIGH finding
|
||||
- [x] Cycle 2 deltas (AZ-498, AZ-499) explicitly reviewed for regressions
|
||||
@@ -0,0 +1,159 @@
|
||||
# Static Analysis — Azaion UI
|
||||
|
||||
**Date**: 2026-05-12
|
||||
**Scope**: `src/` (production SPA), `mission-planner/src/` (port-source — NOT shipped in production bundle but in git history), `nginx.conf`, `.env.example` files
|
||||
**Method**: targeted ripgrep patterns + manual review, complementing the 32 existing static checks in `scripts/run-tests.sh` (STC-SEC*, STC-N*, STC-S*, STC-ARCH-*)
|
||||
**Cycle**: Phase B / Cycle 2
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Severity | Count | New in this audit |
|
||||
|----------|-------|-------------------|
|
||||
| Critical | 0 | — |
|
||||
| High | 1 | F-SAST-1 (Google Geocode API key in `mission-planner/`) |
|
||||
| Medium | 2 | F-SAST-2 (`unpkg.com` CDN ref in `mission-planner/`), F-SAST-3 (`mission-planner` not covered by `STC-SEC2`) |
|
||||
| Low | 1 | F-SAST-4 (port-source still uses third-party tile fallbacks) |
|
||||
|
||||
**No NEW Critical or High findings in `src/` (production bundle).** All High-severity findings are confined to `mission-planner/` — the inferior port-source documented in `_docs/02_document/components/05_flights/description.md` as "not built; manual reference for porting work".
|
||||
|
||||
The 32 existing static checks (run-tests.sh) cover: no eval/Function, no `dangerouslySetInnerHTML`, no token logging, no `innerHTML=` writes, no banned ML/crypto/persistence libs, no hardcoded `/api` literals, TS strict mode, no `target=_blank`, no OWM key in `src/` or `mission-planner/`. All passed in the Cycle 2 test run (`_docs/03_implementation/test_run_report_phase_b_cycle2.md`).
|
||||
|
||||
---
|
||||
|
||||
## Findings
|
||||
|
||||
### F-SAST-1 — Hardcoded Google Geocode API key in `mission-planner/src/config.ts` — HIGH
|
||||
|
||||
**Location**: `mission-planner/src/config.ts:2`
|
||||
|
||||
```ts
|
||||
export const GOOGLE_GEOCODE_KEY = 'AIzaSyAhvDeYukuyWVrQYbRhuv91bsi_jj5_Iys';
|
||||
```
|
||||
|
||||
**Used by**: `mission-planner/src/flightPlanning/LeftBoard.tsx:114-115`
|
||||
|
||||
```ts
|
||||
`https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(address)}&key=${GOOGLE_GEOCODE_KEY}`
|
||||
```
|
||||
|
||||
**Production-bundle exposure**: NONE today. `src/` does NOT import from `mission-planner/` (verified via grep: zero matches for `from '.*mission-planner` in `src/`). The `Dockerfile` builds only the main project (`bun run build` produces `dist/` from the `src/` Vite root). `mission-planner/` is a port-source kept around for reference per `_docs/02_document/components/05_flights/description.md` line 59.
|
||||
|
||||
**Git-history exposure**: HIGH. The key is committed and visible to anyone who clones the repository, has read access to the upstream remote, or reads any historical revision. Same threat class as the OpenWeatherMap key resolved in **AZ-499** (`_docs/00_problem/security_approach.md` §5).
|
||||
|
||||
**Risk**:
|
||||
- Quota/rate-limit theft (Google charges per geocode call past the free tier).
|
||||
- Provider account abuse — whoever owns the Google Cloud billing account is liable.
|
||||
- Accelerated risk if `mission-planner/` is ever ported into the production SPA without this finding being remediated first.
|
||||
|
||||
**Remediation** (mirrors AZ-499 / AC-42 pattern):
|
||||
1. **Revoke the key** at https://console.cloud.google.com/google/maps-apis/credentials (manual, OUT-OF-BAND, USER ACTION). Capture evidence per the AZ-499 AC-7 protocol.
|
||||
2. Externalize: `import.meta.env.VITE_GOOGLE_GEOCODE_KEY` in `mission-planner/src/config.ts`. Fail-soft if unset (mirror `WeatherService.ts` pattern from AZ-499).
|
||||
3. Update `mission-planner/.env.example` to advertise the new variable + the `<your-google-geocode-api-key>` placeholder.
|
||||
4. Extend the `owm_key_in_source` static-check pattern in `tests/security/banned-deps.json` to also block the literal `AIzaSyAhvDeYukuyWVrQYbRhuv91bsi_jj5_Iys` (defense-in-depth — does not replace revocation).
|
||||
5. Long-term: when geocoding lands in the production SPA, route via a suite-side proxy (no client-visible key — same architecture decision noted in `security_approach.md` §5 for OWM).
|
||||
|
||||
**Recommended ticket**: `AZ-NEW (Phase B) — Externalize Google Geocode key in mission-planner port-source` (mirror AZ-499 structure).
|
||||
|
||||
---
|
||||
|
||||
### F-SAST-2 — `unpkg.com` CDN reference in `mission-planner/` — MEDIUM
|
||||
|
||||
**Location**: `mission-planner/src/icons/PointIcons.tsx:7`
|
||||
|
||||
```ts
|
||||
iconUrl: 'https://unpkg.com/leaflet@1.7.1/dist/images/marker-icon.png',
|
||||
```
|
||||
|
||||
**Production-bundle exposure**: NONE today (same reason as F-SAST-1).
|
||||
|
||||
**Risk class**:
|
||||
- Supply-chain: a compromised unpkg.com or a take-over of `leaflet@1.7.1` could replace the icon with a tracking pixel or attack payload.
|
||||
- Privacy: the browser leaks the user's IP + referer to a third-party CDN on every page load that uses these icons.
|
||||
- Air-gap incompatibility: the suite is documented as "air-gapped friendly" (`_docs/02_document/architecture.md`); a CDN dependency violates that.
|
||||
|
||||
The main `src/` is already protected: `STC-SEC2` (`scripts/run-tests.sh`) blocks `unpkg.com` in `src/`. **`mission-planner/` is currently NOT scanned by STC-SEC2** — see F-SAST-3.
|
||||
|
||||
**Remediation**:
|
||||
- Replace with a relative import (the `leaflet` package is already a dependency; bundling the marker icon locally is one line).
|
||||
- OR move this asset into the same-origin nginx static path during the eventual port.
|
||||
|
||||
**Recommended ticket**: bundle into the same Phase B port-source cleanup task as F-SAST-1.
|
||||
|
||||
---
|
||||
|
||||
### F-SAST-3 — `STC-SEC2` (no-CDN gate) does NOT scan `mission-planner/` — MEDIUM
|
||||
|
||||
**Location**: `scripts/run-tests.sh` (the `src_grep` helper passes `src` only for STC-SEC2)
|
||||
|
||||
**Evidence**: `STC-SEC2` is currently `src/`-scoped only; the `owm_key_in_source` and `alert_calls` checks were widened in AZ-499/AZ-466 to scan both `src/` and `mission-planner/` (see `scripts/check-banned-deps.mjs:204`), but the `unpkg.com`/CDN deny-pattern was not.
|
||||
|
||||
**Risk**: a future port that copies more `mission-planner/` code into `src/` could re-introduce CDN URLs that the current static gate would not catch on the source side.
|
||||
|
||||
**Remediation**:
|
||||
- Move the no-CDN check into `tests/security/banned-deps.json` as a new section (e.g. `cdn_in_source`) and let `check-banned-deps.mjs` apply it to both roots, mirroring the AZ-499 widening pattern.
|
||||
- Add the new STC-ID to `_docs/02_document/tests/security-tests.md`.
|
||||
|
||||
**Recommended ticket**: `AZ-NEW (Phase B) — Widen no-CDN static gate to cover mission-planner/` (small, 2-3 SP).
|
||||
|
||||
---
|
||||
|
||||
### F-SAST-4 — Port-source still uses third-party tile fallbacks — LOW
|
||||
|
||||
**Location**: `mission-planner/src/constants/tileUrls.ts:2-3`, `mission-planner/.env.example:25`
|
||||
|
||||
```ts
|
||||
export const TILE_URLS = {
|
||||
classic: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
satellite: import.meta.env.VITE_SATELLITE_TILE_URL || 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
|
||||
};
|
||||
```
|
||||
|
||||
**Production-bundle exposure**: NONE today (port-source is not built).
|
||||
|
||||
**Risk**: if `mission-planner/` is ever shipped, AZ-498's gains are partially undone — the classic tile path goes straight to OSM with NO env override path, and the satellite fallback hits ArcGIS unauthenticated.
|
||||
|
||||
**Remediation**: deferred to whichever ticket consumes / replaces the `mission-planner/` port-source in the SPA. Do NOT fix in-place — `mission-planner/` is documented as inferior and slated for removal once the port is complete.
|
||||
|
||||
---
|
||||
|
||||
## Negative findings (clean — explicitly verified)
|
||||
|
||||
| Pattern | `src/` | `mission-planner/src/` | Coverage |
|
||||
|---------|--------|------------------------|----------|
|
||||
| `eval(`, `new Function(`, `setTimeout('...')` | clean | clean | manual grep this audit |
|
||||
| `dangerouslySetInnerHTML`, `innerHTML=`, `outerHTML=`, `document.write` | clean | clean | manual grep this audit |
|
||||
| `target='_blank'` (without `rel='noopener'`) | clean | clean | manual grep this audit |
|
||||
| `console.log`/`console.error` of `token`/`bearer`/`password`/`secret`/`key`/`cookie`/`auth` | clean | clean | manual grep this audit |
|
||||
| `__proto__`, `constructor[…]`, `prototype[…]` (prototype-pollution patterns) | clean | clean | manual grep this audit |
|
||||
| `localStorage`/`sessionStorage`/`indexedDB` writes of bearer | clean (only test-fixture reads in `auth/AuthContext.test.tsx`) | clean | STC-SEC3 + manual grep |
|
||||
| `credentials: 'include'` on every authed fetch | present on the 401-recovery path (`src/api/client.ts:90`); KNOWN MISSING on bootstrap refresh (`src/auth/AuthContext.tsx:24`, quarantined test acknowledges Step 4 fix) | n/a (no auth in port-source) | KNOWN — `security_approach.md` §1 finding F2 |
|
||||
| Hardcoded OWM key `335799082893fad97fa36118b131f919` | clean | clean | STC-SEC1 + STC-SEC1B + STC-SEC1C (AZ-499) |
|
||||
| Hardcoded URLs other than the OWM endpoint | clean (only `flightPlanUtils.ts:59` `DEFAULT_OWM_BASE_URL` — env-overridable fallback) | F-SAST-1, F-SAST-2, F-SAST-4 above | manual grep this audit |
|
||||
| Other API-key formats: `AIza…`, `sk_live_`, `pk_live_`, `xox*`, `ghp_`, `AKIA…`, generic 32+ hex | clean | F-SAST-1 only | manual grep this audit |
|
||||
| `password = '...'`/`secret = '...'`/`api_key = '...'` literals | clean (only `password` field labels and `AdminPage.tsx` form bindings) | clean | manual grep this audit |
|
||||
|
||||
---
|
||||
|
||||
## Cycle-2 delta — security review of AZ-498 + AZ-499 changes
|
||||
|
||||
| Change | Security review |
|
||||
|--------|----------------|
|
||||
| `src/features/flights/FlightMap.tsx`, `MiniMap.tsx` — `<TileLayer crossOrigin="use-credentials" url={getTileUrl()}/>` | OK. `crossOrigin="use-credentials"` only sends cookies to the SAME origin (`/tiles/{z}/{x}/{y}`) when production env points at the same-origin nginx path. Dev default `http://localhost:5100/...` is HTTP and DEV-ONLY (acknowledged in `.env.example:12-16`). Confirms cookie ride for tile auth without exposing the bearer. |
|
||||
| `src/features/flights/types.ts:63` — `DEFAULT_SATELLITE_TILE_URL = 'http://localhost:5100/tiles/{z}/{x}/{y}'` | OK. Dev default; production `.env` MUST override. The `.env.example` documentation is explicit. No bearer leakage path. |
|
||||
| `mission-planner/src/services/WeatherService.ts` — env-resolved key + base URL + fail-soft | OK — matches AZ-499 spec. Key never re-introduced in source (verified by STC-SEC1C). |
|
||||
| `STC-SEC1C` static gate in `scripts/run-tests.sh` | OK. Defense-in-depth as designed; widens the `STC-SEC1*` family to scan `mission-planner/` for the literal OWM key. |
|
||||
| Tests `src/features/flights/__tests__/satellite_tile.test.tsx`, `tests/mission_planner_weather.test.ts` | OK. Tests do not contain real secrets; they `vi.stubEnv` with placeholder strings. |
|
||||
|
||||
No security regressions introduced by Cycle 2.
|
||||
|
||||
---
|
||||
|
||||
## Self-verification
|
||||
|
||||
- [x] Both source trees scanned (`src/`, `mission-planner/src/`)
|
||||
- [x] Each finding has a file path + line number + extract
|
||||
- [x] Test files explicitly excluded from finding lines (only quoted as evidence of negative results)
|
||||
- [x] Cycle 2 deltas individually reviewed
|
||||
- [x] Existing static checks not duplicated — only NEW findings or coverage gaps reported
|
||||
Reference in New Issue
Block a user