Skip to content

security: harden /upload, /new_module, /heartbeat against anonymous abuse (closes #228, closes #229) - #269

Merged
cofade merged 1 commit into
mainfrom
fix/228-229-upload-newmodule-heartbeat-hardening
Aug 22, 2026
Merged

security: harden /upload, /new_module, /heartbeat against anonymous abuse (closes #228, closes #229)#269
cofade merged 1 commit into
mainfrom
fix/228-229-upload-newmodule-heartbeat-hardening

Conversation

@cofade

@cofade cofade commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator

Summary

Both internet-reachable, credential-free trust boundaries flagged by the 2026-08-18 repo audit (epic #262) had no bounds beyond "accepts the request":

Also ships, deliberately as documentation/template rather than code: ADR-032 records the device-identity decision (an interim compiled-in fleet key) for a separate follow-up issue, and deploy/nginx/highfive-ingest.conf is a syntax-verified (nginx -t via Docker) rate/body-size-limit template for an operator to apply to the live host — not yet deployed there.

Review history

This branch went through three independent rounds of the mandated senior-reviewer gate:

  • Round 1 found a real P0: the first-pass lat/lng UPSERT guard in add_module inverted its own gating condition and still let an anonymous re-POST relocate a placed module — verified end-to-end (one POST moved a module from Germany to Sydney).
  • Round 2 re-verified the P0 fix across 11 scenarios (including the pre-existing PR-II/esp: getGeolocation race on first boot — module appears at Null Island until manual RST #89 recovery path, to confirm no regression) and found a real functional bug: OPENCV_IO_MAX_IMAGE_PIXELS was hardcoded separately from MAX_IMAGE_DIM, so the troubleshooting doc's own advice to raise the latter would have silently broken hole detection. Both fixed and verified (the OpenCV fix via a subprocess test, since the protection is native-library state that a same-process module reload can't exercise).
  • Round 3 was a documentation-drift sweep — a couple of docs/comments still described the pre-fix behaviour after the code changed. Fixed, plus a tree-wide grep sweep (not just docs/) to confirm no stale copies remained.

Full write-ups of both incidents (the CASE inversion and the drift pattern) are in docs/11-risks-and-technical-debt/README.md.

Test plan

All run locally, all green:

  • cd image-service; py -m pytest tests/ -q — 181 passed
  • cd duckdb-service; py -m pytest tests/ -q — 304 passed
  • cd backend; npm test — 296 passed
  • py -m pytest tests/e2e/ -v (real docker compose stack) — 6/6 passed
  • Playwright UI suite (docker compose -f tests/ui/docker-compose.ui.yml ... ; npx playwright test) — 16/16 passed (1 pre-existing, unrelated skip)
  • make check-citations and all other pre-push guard scripts — clean
  • nginx -t against the merged deploy/nginx/highfive-ingest.conf + site config (Docker, nginx:alpine) — syntax valid

🤖 Generated with Claude Code

…buse (closes #228, closes #229)

Both trust boundaries flagged by the 2026-08-18 audit were internet-reachable
and credential-free with no bounds beyond "accepts the request":

- image-service /upload never inspected upload bytes, so a stored .html/.svg
  could be served back same-origin with a sniffed Content-Type, and a crafted
  JPEG could decompression-bomb the single-process service. Fixed by
  services/image_guard.py::probe_jpeg (header-only JPEG validation, no pixel
  decode) running before anything is saved, plus a forced .jpg stored
  extension and a pinned image/jpeg Content-Type on the serve routes.

- duckdb-service /new_module let any anonymous re-POST overwrite a placed
  module's name/email/location (public-map-defacement) and spammed Discord on
  every call; /heartbeat let an unregistered MAC grow module_heartbeats
  without bound. Fixed by preserving identity fields unless unset, dropping
  heartbeats for unknown MACs, clamping battery, and removing two raw-body/
  precise-coordinate log lines that leaked into the admin-readable log ring.

Also ships (deliberately not code): ADR-032 records the device-identity
decision (interim fleet key) for a follow-up issue, and deploy/nginx/
highfive-ingest.conf is a syntax-verified (nginx -t) rate-limit template for
an operator to apply to the live host.

Went through three independent senior-review rounds. Round 1 caught a P0: the
first-pass lat/lng UPSERT guard inverted its own gating condition and still
let a re-POST relocate a placed module (verified end-to-end: one POST moved
a module from Germany to Sydney) — fixed and re-verified across 11 scenarios
in round 2. Round 3 was documentation-drift sweeps. See docs/11-risks-and-
technical-debt/README.md for both incidents' write-ups.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@cofade
cofade merged commit 205bcb7 into main Aug 22, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant