Files
ui/_docs/00_problem/security_approach.md
T
Oleksandr Bezdieniezhnykh 510df68bcf [AZ-447] autodev Steps 1-4 baseline: docs, tests, refactor specs
Captures the full output of autodev existing-code Phase A through
Step 4 (Code Testability Revision) for the Azaion UI workspace:

- Step 1 Document: _docs/02_document/ (FINAL_report, architecture,
  glossary, components/, modules/, diagrams/, system-flows,
  module-layout) plus _docs/00_problem/ + _docs/01_solution/ +
  _docs/legacy/ + _docs/how_to_test + README.
- Step 2 Architecture Baseline: architecture_compliance_baseline.md.
- Step 3 Test Spec: _docs/02_document/tests/ (environment,
  test-data, blackbox/performance/resilience/security/
  resource-limit tests, traceability-matrix), enum_spec_snapshot,
  expected_results/results_report.md (98 rows), plus the
  run-tests.sh + run-performance-tests.sh runners.
- Step 4 Code Testability Revision: 01-testability-refactoring/
  run dir (list-of-changes C01-C07, deferred_to_refactor,
  analysis/research_findings + refactoring_roadmap) and the 7
  child task specs AZ-448..AZ-454 under _docs/02_tasks/todo/
  plus _dependencies_table.md.
- _docs/_autodev_state.md pins the cursor at Step 4 / refactor
  Phase 4 entry so /autodev resumes cleanly.

Epic AZ-447 (UI testability gates) tracks the 7 child tasks that
will land in subsequent commits.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 00:38:49 +03:00

13 KiB

Security Approach — Azaion UI

Output of /document Step 6e. Retrospective security view of the SPA grounded in code (src/auth/AuthContext.tsx, src/api/client.ts, src/api/sse.ts), config (nginx.conf, Dockerfile, .woodpecker/build-arm.yml), and the verified architecture (_docs/02_document/architecture.md § 7). Every claim cites its evidence.

Status: synthesised-from-verified-docs (Step 6e — /document) Date: 2026-05-10


Threat model summary

The UI is operator-internal, not public. The trust model is:

  • Trusted: the suite services (reached via nginx reverse-proxy on the same origin); the suite's identity provider (admin/); the operator's authenticated browser session.
  • Untrusted: the browser itself (XSS-resistant design — bearer in memory only); operator network if not on the suite VPN; OpenWeatherMap (currently exfiltrated to via a hardcoded key — finding); OSM tile servers (read-only third-party).

Primary threats considered: token theft via XSS; CSRF via cookie auto-attach; bearer leakage via SSE query string; secret leakage in bundle; privilege escalation via missing client-side route gates; clickjacking / framing.


1. Authentication

Login

  • POST /api/admin/auth/login with { email, password }.
  • admin/ service responds with:
    • Bearer JWT in the response body — held in AuthContext memory only (never written to localStorage / sessionStorage, P3).
    • Secure HttpOnly SameSite=Strict refresh cookie — issued by server, scoped to the suite origin.

Source: src/auth/AuthContext.tsx; architecture.md § 7.

Session bootstrap (cold load)

  • On mount, AuthContext attempts GET /api/admin/auth/refresh to obtain a new bearer.
  • Bug: this call is missing credentials:'include' — the HttpOnly refresh cookie is NOT sent → cold-load refresh fails → user is redirected to /login even with a valid cookie. Step 4 fix candidate.

Source: src/auth/AuthContext.tsx:24; 04_verification_log.md F2.

401-retry path

  • The 01_api-transport client.ts wraps every authenticated fetch. On 401 it issues POST /api/admin/auth/refresh with credentials:'include', replaces the bearer in AuthContext, and retries the original request.
  • This path is correct and is the working refresh mechanism today.

Source: src/api/client.ts:44; 04_verification_log.md F2.

Logout

  • POST /api/admin/auth/logout — clears the bearer in memory; server invalidates the refresh cookie.

Source: src/auth/AuthContext.tsx.

Pre-port (legacy WPF)

  • The WPF-era encrypted-creds command-line handoff (binary-split key fragments + DPAPI) is intentionally not ported — the browser cannot participate in that handoff and the suite identity infrastructure now lives server-side. P8.

Source: _docs/legacy/wpf-era.md §11.


2. Authorization

  • RBAC is server-enforced — every authenticated endpoint validates User.role + permissions[] server-side.
  • The UI inspects AuthUser.role to render or hide nav links and pages, but does NOT treat the result as a security gate.
  • Browser is treated as untrusted; every action confirms with the server.

Findings

  • /admin route lacks a client-side role-gate (PRIORITY — security finding, AC-22). Server-side 403 IS the authoritative gate, but a non-admin user navigating to /admin today sees the broken admin UI flicker before the server rejects requests. Step 4 / Step 8 fix.
  • /settings route gate is more nuanced — there is no explicit SETTINGS permission code in the suite spec; gating relies on server-side 403. Treat as a soft gate (don't expose the link in the Header for non-admins) rather than a hard redirect.

Source: architecture.md § 7 Authorization; App.tsx.


3. Token handling

Token Lifetime Where it lives Where it appears on the wire
Bearer JWT Short (server-issued; refreshed on 401) AuthContext React state — memory only Authorization: Bearer ${token} header on every authenticated fetch; ?token=${bearer} query string on SSE (ADR-008)
Refresh token Long (server-issued) Secure HttpOnly SameSite=Strict cookie — never accessible to JS Cookie header on POST /api/admin/auth/refresh (and the broken bootstrap GET — Step 4 fix)
X-Refresh-Token header Per-request (long-running video detect) passed in by 01_api-transport for endpoints that need it X-Refresh-Token: ${value} per _docs/10_auth.md. Currently NOT sent on POST /api/detect/${mediaId} for video — long videos can blow the access-token TTL → silent failure. Step 4 fix candidate (finding #29).

Key invariants (P3)

  • Bearer is never written to localStorage / sessionStorage / IndexedDB.
  • Refresh token is never read from JS — HttpOnly enforces this.
  • Code-search regression test: zero matches for localStorage|sessionStorage touching the bearer token in src/.

4. SSE bearer-in-query-string

EventSource cannot send arbitrary headers, so src/api/sse.ts passes the bearer in the URL: ?token=${bearer}.

Trade-offs

  • Bearer is short-lived — minimises window of compromise.
  • HTTPS encrypts the URL on the wire — but the URL still appears in:
    • nginx access logs (mitigation: log redaction at the nginx layer — Step 6 surface; not configured today).
    • Browser history (low risk for SSE URLs, but document).
  • Refresh-rotation breaks open SSE connections — the URL was created with the old bearer; no reconnect logic exists today (Step 8 hardening — AC-24).

Source: src/api/sse.ts; ADR-008; architecture.md § 7.


5. Secrets management

Hardcoded OpenWeatherMap API key — P10 violation

  • File: mission-planner/src/utils/flightPlanUtils.ts:60
  • Value: a 32-char hex key shipped in the production bundle.
  • Risk: anyone with access to the bundle can extract and reuse the key (rate-limit theft; provider account abuse). The key is committed to git history.
  • Fix sequence (Step 4 / Phase B):
    1. Rotate the key at OpenWeatherMap (out-of-band, user action).
    2. Move to envimport.meta.env.VITE_OPENWEATHERMAP_API_KEY read at build time (interim).
    3. Proxy via suite — long-term, route the wind compute through flights/ so no key ever reaches the browser (preferred; per architecture.md § Architecture Vision Open Questions item 8).

Other secrets

  • No other hardcoded keys in src/ per Grep audit at Step 4.
  • Suite service URLs are not secrets (they are docker-network hostnames).
  • The bearer is the only sensitive value in browser memory, and it is short-lived.

Source: P10; architecture.md § Architecture Vision; finding (security).


  • Same-origin via nginx: the SPA is served by the same nginx that reverse-proxies /api/<service>/. The browser sees a single origin → cookies scope cleanly; CORS preflight is unnecessary for the suite endpoints.
  • credentials:'include' is required on every authenticated fetch for the HttpOnly refresh cookie to flow. The 401-retry path (api/client.ts:44) is correct; the bootstrap refresh (AuthContext.tsx:24) is broken.
  • CSRF: SameSite=Strict on the refresh cookie + bearer-in-header on authenticated requests. The bearer header cannot be auto-attached by a cross-origin form submit. No additional CSRF token is used today — the architecture pattern (header-based bearer + SameSite=Strict cookie) obviates it.

Source: src/api/client.ts; nginx.conf; architecture.md § 7.


7. Input validation

  • Server is authoritative. The UI does not duplicate validation logic it cannot guarantee.
  • Numeric inputs in 09_settings use parseInt(v) || 0 — clearing a field silently writes 0 (finding B4, AC-26). Step 4 fix.
  • File upload: react-dropzone filters by MIME / extension client-side; the server is authoritative on virus scanning and size enforcement (client_max_body_size 500M).
  • Annotation save body must include Source, WaypointId, videoTime (currently incomplete — finding #32). The wire format is validated by the annotations/ service.

Source: 09_settings/SettingsPage.tsx; 06_annotations/MediaList.tsx; nginx.conf; finding B4 / #32.


8. Output encoding / XSS surface

  • React 19 escapes JSX text by default — string content is safe.
  • dangerouslySetInnerHTML is not used in src/ (Grep audit).
  • HelpModal ships hardcoded English strings inline — XSS-safe but P6 violation (i18n).
  • Tainted-canvas risk on annotation download (AnnotationsPage.handleDownload finding) — cross-origin image data may taint the canvas; the download silently fails. Pure UX bug, not a security defect, but flagged.

Source: 06_annotations/AnnotationsPage.tsx; HelpModal.tsx.


9. Headers / hardening at the nginx layer

Currently configured

  • nginx serves dist/ and reverse-proxies /api/<service>/ to suite services.
  • client_max_body_size 500M.

Currently MISSING (Step 6 surface)

  • Content-Security-Policy — no CSP header. Recommended starting point: default-src 'self'; img-src 'self' https: data:; connect-src 'self' https://api.openweathermap.org/ https://*.tile.openstreetmap.org/; frame-ancestors 'none'; object-src 'none'.
  • X-Frame-Options: DENY (or covered by CSP frame-ancestors) — clickjacking protection.
  • Referrer-Policy: strict-origin-when-cross-origin.
  • Strict-Transport-Security — depends on suite ingress; document the expected value.
  • X-Content-Type-Options: nosniff.
  • Bearer-redaction in nginx access logs for SSE URLs.

These are nginx config additions (server-side), not SPA changes — but the SPA depends on them for hardening. Track at suite level.

Source: nginx.conf; architecture.md § 7 row "Cross-site / clickjack".


10. Audit logging

  • Server-side concern — the admin/, flights/, annotations/, etc. services are responsible for audit-event emission.
  • The SPA does not emit audit events directly. It does not maintain any client-side audit log.
  • The browser console is the only client-side log surface today; no centralized client telemetry (Step 6 surface — _docs/02_document/deployment/observability.md).

Source: architecture.md § 7 Audit logging.


11. Image / supply-chain

Currently in pipeline

  • Multi-stage Dockerfile: oven/bun:1.3.11-alpine (build) → nginx:alpine (runtime).
  • bun install --frozen-lockfile enforces lockfile fidelity.
  • AZAION_REVISION=$CI_COMMIT_SHA and OCI labels stamped at push time.

Currently MISSING (Step 6 surface)

  • No vulnerability scan (Trivy / Grype) on the produced image.
  • No SBOM emission (Syft / cyclonedx).
  • No image signing (cosign).
  • No dependency audit step in CI (bun audit equivalent — Bun does not yet have a first-party audit; npm audit --omit=dev against the lockfile is a reasonable substitute).

Source: .woodpecker/build-arm.yml; architecture.md § 3 "Missing from the pipeline today".


12. Findings → Fix Map

Finding AC Fix step
Bootstrap refresh missing credentials:'include' (F2) AC-01 Step 4 (Code Testability Revision)
Bearer-in-query SSE — refresh-rotation breaks subscription AC-24 Step 8 (Refactor — optional) or Phase B
Hardcoded OpenWeatherMap key (P10) AC-20 Step 4 (env move); Phase B (suite proxy)
/admin route lacks role-gate AC-22 Step 4
09_settings numeric input writes 0 on empty AC-26 Step 4
09_settings save handlers leak saving:true on PUT failure AC-27 Step 4
AdminPage.handleDeleteClass lacks ConfirmDialog AC-30 Step 4
MediaList uses alert() AC-14 Step 4
ConfirmDialog lacks aria-modal/role=dialog AC-15 Step 4 / Step 8
Header dropdown lacks combobox/expanded/Esc/focus-trap AC-16 Step 4 / Step 8
Annotation save body missing Source, WaypointId, wrong time field AC-05 Step 4
X-Refresh-Token not sent on long-video detect (#29) Step 4
Numeric enum drift (AnnotationStatus, MediaStatus, Affiliation, CombatReadiness) AC-04 Step 4 (P9 alignment)
No CSP / hardening headers in nginx.conf Step 6 — track at suite level
No vulnerability scan / SBOM / image signing in CI Phase B

13. Compliance / standards

The UI does NOT claim conformance to any specific standard today:

  • No WCAG-level declaration (multiple a11y findings recorded).
  • No SOC2 / ISO27001 controls are implemented at the SPA layer (server-side concern of the suite).
  • No FIPS / specific crypto-mode requirements at the SPA layer (TLS is terminated server-side; bearer JWT signing is server-side).

These are recorded as anti-criteria (AC-N4) — the UI is internal, operator-only, and trusts the suite for compliance enforcement. Phase B may revisit if a regulated deployment surface emerges.