Skip to content

Claude/security fixes master - #4

Merged
MatMatt merged 2 commits into
masterfrom
claude/security-fixes-master
Aug 6, 2026
Merged

Claude/security fixes master#4
MatMatt merged 2 commits into
masterfrom
claude/security-fixes-master

Conversation

@MatMatt

@MatMatt MatMatt commented Aug 6, 2026

Copy link
Copy Markdown
Owner

No description provided.

claude added 2 commits August 6, 2026 09:39
… eviction panic, broken downloads

Five defects, each with a regression test.

1. Arbitrary file read via GET /api/chunks/{sha} (unauthenticated)

   ChunkStore::chunk_path never validated its argument, which arrives straight
   from a URL path segment. hash[2..4] of "../../../etc/passwd" is "/.", and
   PathBuf::join on an absolute component discards the store prefix, so the
   path resolved to /etc/passwd. Verified against a running node before the
   fix. chunk_path now returns Option<PathBuf> and rejects anything that is
   not 64 lowercase hex chars; get/has/verify/delete all handle the rejection.

2. Remote panic on the same endpoint

   hash[..2] panicked for input shorter than two bytes or on a non-ASCII char
   boundary, so GET /api/chunks/a reset the connection. Same validation covers
   it; the endpoint now returns 404.

3. POST /api/replicate and POST /api/peers were unauthenticated

   /api/replicate takes a caller-supplied peer_url, fetches it and stores the
   response — an SSRF primitive plus arbitrary store pollution. A registered
   peer is not passive either: the heartbeat loop contacts it every 60s and
   auto-replication pulls from it every 5 minutes, so one anonymous request
   set up a standing ingestion channel from an attacker's server. Both now go
   through authorize() at AccessLevel::Write.

4. Auto-eviction task panicked on first run

   eviction::build_replica_map_from_beacon used reqwest::blocking, which
   builds and drops its own runtime. Called from the tokio::spawn eviction
   loop in server.rs, that panics with "Cannot drop a runtime in a context
   where blocking is not allowed" — the task died the first time storage
   exceeded the limit, so nodes never evicted and grew past their cap
   indefinitely. Switched to ureq (already a dependency, genuinely
   synchronous, safe from the CLI too) and wrapped the call in
   block_in_place so it no longer stalls an async worker.

5. GET /api/download/{collection}/{item} returned 500 for every item

   reconstruct_cog only understood the tiled layout and bailed on
   "Missing: earthgrid:width". Nothing writes that metadata: ingest_raster is
   the sole producer and has no callers, so every item comes from ingest_file,
   which stores sequential raw byte chunks. Those concatenate back to the
   original COG exactly, so reconstruct_cog now falls back to reconstruct_raw,
   which reassembles in order and checks the result against the
   earthgrid:file_hash recorded at ingest — a missing chunk is a hard error
   rather than a truncated download.

Also: the beacon federation WebSocket (/api/beacon/ws) accepted any client and
handed it apply_remote_event, including NodePruned, which deletes any node from
the registry by ID. It now requires the shared grid key when one is configured,
and the outbound dialer presents that key. With no key set the socket stays open,
matching the node's open-mode behaviour elsewhere — federated beacons must set
EARTHGRID_API_KEY to the same value on each host.

Tests: 156 passing, up from 147. Traversal, panic and the auth changes were also
verified end to end against a running node from a non-loopback address.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Qjes4orjaT3k6ShNXj5Axm
The previous commit gated /api/beacon/ws on EARTHGRID_API_KEY. That was wrong
in two ways.

1. It reused the node's write credential. The dialer has to transmit whatever
   the listener checks, so every beacon you federated with would learn your
   EARTHGRID_API_KEY and could then drive your whole node API — /api/fetch,
   /api/replicate, the fetch queue. A federated peer needs exactly one
   capability, registry sync, so it now gets exactly one credential:
   EARTHGRID_FEDERATION_KEY, sent in x-earthgrid-federation-key.

2. It failed open. The check only engaged when a key happened to be set, so a
   beacon in the default keyless configuration still accepted any client — and
   a client on that socket can add nodes to the registry and, via NodePruned,
   delete any node by ID. Federation now fails closed: with no key configured
   the endpoint returns 503 and spawn_peer_connections refuses to dial. An
   unconfigured beacon federates with nobody instead of with everybody.

Key comparison is constant-time.

Verified against two live beacons:
  no key configured  — 503 for no credential, for a guessed one, and for the
                       old x-api-key header
  key configured     — 401 for no credential, 401 for a wrong key,
                       101 Switching Protocols for the right one

docs/beacon-guide.md documents the new variable, that it must match across
beacons, that it is not the grid key, and that peer URLs should be https since
the key travels in a handshake header.

Tests: 158 passing.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Qjes4orjaT3k6ShNXj5Axm
@MatMatt
MatMatt merged commit 342bc10 into master Aug 6, 2026
3 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.

2 participants