fleetops/docs/260702_audit_report.md
david kiania a0022fbeaf fix(security): escape API strings, pin CDN scripts, add CSP (FO-SEC-01/02/03)
The tickets code escaped HTML, but the logistics + fuel renderers and the error
banners interpolated API strings straight into innerHTML. Fuel Log fields
(driver, department, fuel_type, plate) come from WhatsApp messages and
vehicle/driver names from the Tracksolid registry — both user-controlled — so
this was a stored-XSS path into every dispatcher's browser.

- Hoist escapeHtml into HELPERS + add esc(); route every logistics/fuel renderer
  and the three error banners through it (21 -> 37 escaped call sites).
- SRI integrity + crossorigin on Chart.js 4.4.1 and maplibre-gl 4.7.1 JS/CSS.
- Caddyfile: CSP (self + pinned CDNs + CARTO basemap + the two fleet APIs),
  X-Content-Type-Options, Referrer-Policy, frame-ancestors 'none', -Server.
  Validated with `caddy validate` inside the deployed image.
- loadLive(): check r.ok; pause the 15s live poll while hidden or off the Tickets
  tab, refresh immediately on return (FO-BUG-01/02).
- Missing-API_BASE fallback flipped staging -> prod, matching the documented
  design (FO-OPS-01).

Inline app script passes `node --check`. Audit + plan + work log in docs/260702_*.
Local only; not deployed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 09:47:40 +03:00

67 lines
3.5 KiB
Markdown

# FleetOps — Platform Audit Report (2026-07-02)
Part of the 2026-07-02 cross-repo audit (tracksolid + fleettickets + fleetops; see
the tracksolid repo's `docs/reports/260702_platform_audit_report.md` for the
DB/host-wide findings). Scope here: the SPA (`src/index.html` + `src/env.js`),
the Caddy/Docker serving layer, and the two deployed containers on twala.
Companion docs: `260702_fix_plan.md`, `260702_work_done.md`.
---
## High
### FO-SEC-01 — Stored XSS: API strings rendered into `innerHTML` unescaped
The tickets explorer and map popups escape correctly (`escapeHtml`), but the
**logistics tables (`renderVehicles`, `renderDrivers`), the fuel banner notes, and
all three Fuel Log tables (`renderFuelVehicles`, `renderFuelDepartments`,
`renderFuelRecent`)** interpolate API strings straight into `innerHTML`. The Fuel
Log fields (`driver`, `department`, `fuel_type`, `plate`) originate from **WhatsApp
messages** — genuinely user-generated content — and vehicle/driver names come from
the Tracksolid registry, editable by any fleet-console user. A crafted name like
`<img src=x onerror=…>` executes in every dispatcher's browser. Error banners also
render `e.message` unescaped (server-influenced text).
### FO-SEC-02 — CDN scripts loaded without Subresource Integrity
Chart.js (jsdelivr) and MapLibre (unpkg) load with no `integrity`/`crossorigin`
attributes — a compromised or MITM'd CDN response executes arbitrary script with
access to the fleet APIs.
### FO-SEC-03 — No security headers from Caddy
No `Content-Security-Policy`, `X-Content-Type-Options`, `Referrer-Policy`, or
frame-ancestors protection. Combined with FO-SEC-01 this leaves injected markup
free to load external script and exfiltrate anywhere.
## Medium
### FO-BUG-01 — `loadLive()` parses the response without checking `r.ok`
A 4xx/5xx from `/webhook/live-positions` throws inside `r.json()` on non-JSON
bodies with an unhelpful error; harmless but noisy.
### FO-BUG-02 — Live-position poll never pauses
The 15-second `/webhook/live-positions` poll starts when the Tickets map is first
opened and then runs forever — including when the user is on the Logistics/Fuel
tabs and when the browser tab is hidden overnight. A dashboard left open ≈ 5,760
wasted API calls/day per viewer.
### FO-OPS-01 — Fallback API base pointed at *staging*
`index.html` fell back to `https://fleetapi.fivetitude.com` when `env.js` isn't
templated. CLAUDE.md documents the intended fallback as the **prod** API. A prod
pod missing `API_BASE` would silently lean on the staging bridge (and fail CORS
confusingly).
## Notes / by design
- **FO-OPS-02** — the prod container (`API_BASE=fleetapi.rahamafresh.com`) runs
commit `21bca24`, ~19 commits behind HEAD — this is the documented frozen-prod
staging-umbrella pattern, not drift. But note: **promotion must pair the SPA
deploy with the prod `dashboard_api` bridge redeploy** — prod's bridge currently
lacks all `/webhook/{inc,crq}-*` and `/analytics/fuel-fills` routes the newer
SPA calls (see the tracksolid audit, OPS-01).
- **FO-OPS-03** — untracked scratch files at the repo root
(`marker-preview.html`, `tracksolid_db_connection.md`, `webook_instructions.txt`).
`tracksolid_db_connection.md` documents the public `twala:5433` superuser
connection pattern that the tracksolid audit is eliminating — update or remove
once the SSH-tunnel workflow lands.
- The serving layer is otherwise good: Caddyfile validated at build time,
healthcheck endpoint, runtime `API_BASE` injection via `templates`, `no-store`
on the shell and env.js.