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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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