| Commit message (Collapse) | Author | Age | Files | Lines |
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Phase 11.5.10.
POST /v1/nodes/announce accepted any pk_node with no proof the announcer held
the matching private key, so a user could register a node record carrying
someone else's node key, and records accumulated without limit.
The announcer now signs a domain-separated message binding the key to their
account — meshbay:node_announce:{user_id}:{pk_node}:{timestamp} — reusing the
shape already proven by /v1/nodes/auth, so a signature for one can never
satisfy the other. Same 60-second window.
Re-announcing the same key now updates the existing record in place instead of
creating a new row.
Three test helpers had to be taught to sign, which is the useful part: nothing
in the suite had ever exercised announce with an attacker's key. The new tests
cover the missing proof, a foreign key, a stale timestamp, and idempotence.
Note for the record: the node key is independent of the user's identity key.
Two hub tests asserted the announced pk_node equalled the user's pk_ed, which
happened to be true only because the daemon announces its keystore key. They
now assert against the announced key itself.
Tests: 157 hub+common, node suite green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Phase 11.5 — findings H6, C4 (partial), and milestone 11.5.3.
H6 — resource exhaustion. Several paths let one peer degrade or stall a node:
* the DataChannel frame limit was a flat 64 MB applied BEFORE authentication,
so an unauthenticated peer could announce a huge frame and dribble bytes
into it. Unauthenticated peers now get 64 KB; the large budget is granted
only after the GEK proof, where it is needed for uploads.
* _do_stream_segment ran subprocess.run(..., timeout=30) directly in the event
loop, stalling the entire daemon — every peer, every group — for up to
thirty seconds per request. Now async, with a timeout and process kill.
* ffmpeg was spawned per stream request with no cap. Both streaming paths now
share a transport-wide semaphore.
* POST /v1/nodes/{id}/webrtc/offer was reachable by any authenticated user for
any node, with no membership check and no rate limit, making the target node
allocate an aiortc PeerConnection and gather ICE on demand — remote resource
exhaustion against a third party's machine. Now rate limited, capped per
user, SDP size bounded, and the caller must share an active group with the
node. That also closes the H4 gap where signaling ignored group status.
* POST /v1/nodes/{id}/incoming took peer_ip verbatim, so any user could make an
arbitrary node emit UDP packets to an address of their choosing. The probe
target must now match the caller's own source address.
C4 (partial) — the pre-proof bundle window. GEK and keypair bundle fetches are
served before the GEK proof by necessity: the client needs its wrapped bundle in
order to compute the proof. That window is a disclosure surface a hub can reach
by forging a JWT. Bounded to 4 fetches per session and audited as
"pre_proof_fetch". The real fix is removing remote keypair bundles entirely,
which belongs to the native client (Phase 13.3).
11.5.3 — the node admin UI was unauthenticated because it binds loopback. But
any local process can reach it, and so can a page in the operator's browser via
DNS rebinding — and this API re-initialises group keys and reads the audit log.
H2 showed script execution there equals full control. Now gated by a per-run
token, printed at startup, accepted as ?t= or X-MeshBay-Token.
One test needed rewriting rather than adding: the first version asserted
"subprocess.run(" was absent from the source, which also matched the comment
documenting the old behaviour. It now parses the AST and checks the property.
Tests: 121 node, 142 hub+common. Regression suite 47 node + 10 hub.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
|
Phase 11.5 — finding C2 (see second-review.md).
/v1/nodes/ws took node_id and group_ids straight from the client's first
message with no ownership check:
node_id = msg.get("node_id") or decoded.get("sub", "unknown")
_connected_nodes[node_id] = ws
Any registered user could connect with an ordinary browser token, claim a
victim node's id and overwrite its entry. Every WebRTC offer for that node was
then relayed to the attacker, who answered with their own SDP — full node
impersonation. The DTLS channel binding does not help, because the attacker is
the endpoint rather than a relay: the browser sends its GEK proof to the
attacker, who ignores it and replies handshake_ack. The attacker received the
victim's encrypted keypair bundle, chat and uploads, and could serve a forged
index.
Registration now requires scope == "node", verifies Node.user_id against the
token subject, checks the account is active, and refuses to displace a live
registration instead of silently overwriting it.
group_ids are intersected with the operator's actual membership: a node may
narrow the set to what it hosts but cannot widen it, so it cannot advertise
itself as an online source for arbitrary groups.
Authorization uses a short-lived session rather than Depends(get_db): a node
WebSocket lives for hours and a request-scoped dependency would pin a
PostgreSQL connection for its whole lifetime.
BEHAVIOUR: a node hosting a group whose hub membership was never recorded for
the operator's account will stop appearing in GET /v1/groups/{id}/nodes.
Adds tests/test_node_ws_auth.py (7 tests). The node WebSocket had no test
coverage at all, which is why this went unnoticed.
Tests: 109 node, 139 hub+common.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|