fleetops/docs/260702_audit_report.md

68 lines
3.5 KiB
Markdown
Raw Normal View History

# 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.