Skip to content

Unsigned announce repoints a peer's http_url and inherits its reachable=true federation gate #270

Description

@beardthelion

An unauthenticated caller can repoint an existing, currently-reachable peer's http_url to a host they control and inherit that peer's reachable=true federation gate, with no probe in between.

Mechanism

upsert_peer (crates/gitlawb-node/src/db/mod.rs:2093-2095):

INSERT INTO peers (did, http_url, last_seen, last_ping_ok, announced_at)
VALUES ($1, $2, $3, FALSE, $3)
ON CONFLICT(did) DO UPDATE SET http_url = $2, last_seen = $3

The conflict branch rewrites http_url and leaves last_ping_ok alone. Reaching it needs no signature: require_signed_peer_writes defaults false (config.rs:48), and the announce handler's unsigned branch only logs a warning (api/peers.rs:196-201). There is no lookup of the existing row, no stored-key check, and no first-writer-wins rule, so one party can rewrite another's row.

Verified by execution

A throwaway #[sqlx::test] seeded a peer with last_ping_ok = TRUE and http_url = https://honest-peer.example.com, then drove the mounted announce handler with no auth extension at all and a body naming the same DID with http_url = https://attacker.example.com. Result: 2xx, the URL was rewritten, and last_ping_ok was still TRUE.

Why it matters

api/repos.rs:1451 filters the federated fan-out on that flag, and the response handling at api/repos.rs:1464-1476 takes the peer's JSON body verbatim and stamps each entry with node_did set to the hijacked peer's DID. No signature check, no content addressing. So the attacker injects arbitrary repo entries attributed to a legitimate peer.

Bound on the attack

A new DID inserts with last_ping_ok FALSE (db/mod.rs:2094) and stays out of the fan-out until a gossip round probes it. So the instant, probe-free version requires hijacking an existing reachable DID. An attacker announcing their own DID with their own live host still enters the fan-out, just after a probe and without stolen attribution.

Fix direction

Reset the gate whenever the URL actually changes, so a repointed peer has to re-earn reachability:

ON CONFLICT(did) DO UPDATE SET
  http_url = $2,
  last_seen = $3,
  last_ping_ok = CASE WHEN peers.http_url IS DISTINCT FROM $2 THEN FALSE ELSE peers.last_ping_ok END

That leaves a plain liveness re-announce alone while closing the carry-over on a swap. It is a direction, not something I have compiled and run; the deeper question is whether announce should bind a DID to its first-seen key regardless of the signing default, which would close the rewrite rather than just its blast radius.

Scope

Pre-existing and untouched by #248: git diff 111cff7e...32e45787 does not include db/mod.rs, and the peers.rs hunks touch only ping_peer and tests. #248 does widen the window slightly in passing, since its two-consecutive-failure hysteresis means a hijacked-then-dead URL holds the gate for two gossip rounds instead of one.

Metadata

Metadata

Assignees

No one assigned

    Labels

    crate:nodegitlawb-node — the serving node and REST APIkind:securityVulnerability fix or hardeningsev:highMajor break or real security/trust risk, no easy workaroundsubsystem:apiNode REST API request/response surfacesubsystem:peersPeer announce, discovery, and registrysubsystem:replicationMirror, replica, and cross-node sync

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions