LC-152-TOCTOU: pin outbound connections to the SSRF-validated IP (close the DNS-rebind re-resolve window) #286

Closed
opened 2026-05-31 04:38:52 +02:00 by longjacksonle · 0 comments

Problem (tracking ticket for a documented accepted-residual)

The LC-152 SSRF guard validates a destination host by resolving it and checking every address against the public-IP filter, then lets the HTTP client (reqwest / ureq) connect. The client performs its OWN DNS resolution to connect. Between the guard's resolve and the client's resolve there is a TOCTOU window: a hostile DNS server can answer public on the first lookup and private on the second (classic DNS rebinding), so a host that passed the filter can still connect to an internal address.

This is a KNOWN, accepted residual, not a new finding. It is documented in prose (docs/protocol-bridges.md:57) and is shared by every outbound path that uses the guard. This ticket exists only so the residual is tracked on the board rather than living solely in a code-adjacent comment, and so any future "is SSRF fully closed?" question has a ticket to point at.

Where (paths sharing the residual)

  • server/src/http_client.rs - the LC-152 two-layer guard (URL-input validation + custom resolver). The custom resolver narrows but does not fully eliminate the window because the literal connect still re-resolves.
  • Outbound callers riding it: outgoing.rs (LC-75 webhook delivery), routes/unfurl.rs, routes/slash.rs, bridge_avatar.rs (LC-78 avatar proxy), push/mod.rs.
  • Desktop sibling: desktop/src/net_guard.rs has a PublicOnlyResolver that closes the resolve-then-connect window for hostnames on its guarded agent (it routes the actual connect through the validated resolution), so the desktop self-updater path is the closest existing example of the mitigation shape.

Failure mode / cost

An attacker who controls DNS for a hostname they can get the server to fetch (a webhook URL, an unfurl target, a bridge avatar URL) can rebind it to an internal address after the guard's check, reaching internal services the public-IP filter was meant to block. Exploitation requires attacker-controlled DNS and a same-host fetch the server initiates; the impact is internal SSRF.

Proposed scope

  • Decide whether to close it or formally accept it. The clean fix is to make the guarded HTTP client connect to the EXACT validated IP (resolve once in the guard, pin the connection to that address) rather than re-resolving, mirroring the desktop PublicOnlyResolver approach, for every outbound path.
  • If closing: thread a pinned-IP connection (custom resolver / connect-to override) through http_client.rs so the address the filter approved is the address connected to, for all callers.
  • If accepting: record the decision + rationale in a single place (the LC-152 module doc) and reference this ticket, so the prose comment is no longer the only record.

Acceptance

Either the guarded client connects only to the address the SSRF filter validated (no second independent resolve), with a test that a rebind-after-check answer is refused; OR a recorded accept-the-risk decision referencing this ticket.

Anchored to LC-152 (server-side SSRF/TOCTOU). Severity: security, low likelihood (needs attacker-controlled DNS), known + documented.

## Problem (tracking ticket for a documented accepted-residual) The LC-152 SSRF guard validates a destination host by resolving it and checking every address against the public-IP filter, then lets the HTTP client (`reqwest` / `ureq`) connect. The client performs its OWN DNS resolution to connect. Between the guard's resolve and the client's resolve there is a TOCTOU window: a hostile DNS server can answer public on the first lookup and private on the second (classic DNS rebinding), so a host that passed the filter can still connect to an internal address. This is a KNOWN, accepted residual, not a new finding. It is documented in prose (`docs/protocol-bridges.md:57`) and is shared by every outbound path that uses the guard. This ticket exists only so the residual is tracked on the board rather than living solely in a code-adjacent comment, and so any future "is SSRF fully closed?" question has a ticket to point at. ## Where (paths sharing the residual) - `server/src/http_client.rs` - the LC-152 two-layer guard (URL-input validation + custom resolver). The custom resolver narrows but does not fully eliminate the window because the literal connect still re-resolves. - Outbound callers riding it: `outgoing.rs` (LC-75 webhook delivery), `routes/unfurl.rs`, `routes/slash.rs`, `bridge_avatar.rs` (LC-78 avatar proxy), `push/mod.rs`. - Desktop sibling: `desktop/src/net_guard.rs` has a `PublicOnlyResolver` that closes the resolve-then-connect window for hostnames on its guarded agent (it routes the actual connect through the validated resolution), so the desktop self-updater path is the closest existing example of the mitigation shape. ## Failure mode / cost An attacker who controls DNS for a hostname they can get the server to fetch (a webhook URL, an unfurl target, a bridge avatar URL) can rebind it to an internal address after the guard's check, reaching internal services the public-IP filter was meant to block. Exploitation requires attacker-controlled DNS and a same-host fetch the server initiates; the impact is internal SSRF. ## Proposed scope - Decide whether to close it or formally accept it. The clean fix is to make the guarded HTTP client connect to the EXACT validated IP (resolve once in the guard, pin the connection to that address) rather than re-resolving, mirroring the desktop `PublicOnlyResolver` approach, for every outbound path. - If closing: thread a pinned-IP connection (custom resolver / connect-to override) through `http_client.rs` so the address the filter approved is the address connected to, for all callers. - If accepting: record the decision + rationale in a single place (the LC-152 module doc) and reference this ticket, so the prose comment is no longer the only record. ## Acceptance Either the guarded client connects only to the address the SSRF filter validated (no second independent resolve), with a test that a rebind-after-check answer is refused; OR a recorded accept-the-risk decision referencing this ticket. Anchored to LC-152 (server-side SSRF/TOCTOU). Severity: security, low likelihood (needs attacker-controlled DNS), known + documented.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/lets-chat#286
No description provided.