Skip to content

Security: SSRF via peer announce endpoint allows internal network probing #85

Description

@kevincodex1

Summary

The unauthenticated POST /api/v1/peers/announce endpoint accepts any http(s) URL as a peer address without validating that the host is publicly routable. An attacker can register loopback, RFC1918, ULA, or link-local addresses as peers. The node then fans out signed
sync-notify POSTs to every registered peer, turning the peer registry into an SSRF probe into the node's internal network.

Impact

On the live node.gitlawb.com instance, 126 of 194 peer rows were poisoned with fake DIDs and localhost/internal targets. An attacker can:

  • Probe internal services (127.0.0.1, 10.x, 192.168.x, 169.254.169.254, etc.) by observing timing/response behavior of the signed fan-out requests.
  • Poison the peer table so legitimate sync traffic is misrouted.
  • Use the node as an amplification point for signed requests to arbitrary internal endpoints.

Affected path

  • crates/gitlawb-node/src/api/peers.rsannounce route
  • crates/gitlawb-node/src/main.rs — sync-notify fan-out

Proposed fix

  1. Validate peer URLs on ingress. Add is_public_http_url() that rejects loopback, unspecified, RFC1918, ULA, link-local, and localhost hosts. Reject invalid URLs at the announce boundary.
  2. Disable redirect following on the sync-notify client. Configure reqwest with redirect::Policy::none(), or a custom policy that re-runs is_public_http_url() against every hop. This closes the SSRF even when a public URL 302s to an internal target.
  3. Route bootstrap announce through the same validator so the bootstrap path can't be used to bypass ingress validation.
  4. Prune poisoned peers at boot. Add Db::prune_non_public_peers() that iterates all rows, logs and continues on per-row failure (do not abort on first error), and removes any peer whose URL fails is_public_http_url().
  5. Tests covering: each rejected address family, redirect-to-internal rejection, bootstrap-path validation, and prune resilience.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions