The directory's refresher fetches LAN and container-network addresses named by an rw peer or by a listed feed's redirect #329

Closed
opened 2026-10-23 05:26:48 -07:00 by rays · 2 comments
Owner

Found by the security-audit skill (run-1, 2026-10-20). Source-read only: nothing was run, so this is a lead that still needs its local check, not a confirmed finding. Two independent verifiers did not disprove it.

Feed URLs reach the directory's refresher from rw-token snapshots (lib.rs snapshot, need(Access::Rw)) and from Apple's lookup feedUrl (apple.rs lookup_entries, publisher-influenced). Store::snapshot keeps any http(s) URL with a host that url_looks_private does not flag, and that function tests only paid hosts, userinfo and credential-like query keys, not destination addresses. Directory::refresh then calls ipx_feed::fetch, which follows up to ten Location headers to any host and connects with req.send() from the directory container's network. So an rw peer can submit http://192.168.1.x:port/... or a Docker service name directly, and the publisher of any already-listed feed can answer the refresher with a 30x to a loopback, LAN or compose-network address. If the target answers with RSS or Atom, its parsed title, description and items are saved (Store::save) and shown on the public pages and /v1; otherwise a listed feed failing for 24 hours shows only the coarse failure_words reason, a slow, low-resolution reachability oracle. Requests are unauthenticated GETs (fetch is called with auth None), so impact depends on what internal services answer to a plain GET.

Claimed root cause. Store::normalise (store.rs:41) accepts any http(s) URL with a host, and the only refusal, ipx_feed::url_looks_private (feed/src/lib.rs:1274), is about credentials and paid hosts. ipx_feed::fetch (feed/src/lib.rs:136-189) follows up to ten redirects to any host, and the directory's reqwest client (lib.rs:137-143) has Policy::none only so fetch sees each hop, with no resolver or connector hook. No loopback, RFC 1918, link-local, ULA or internal-name policy is applied before connecting or after resolution on any hop.

Local check. {'local': "In a sandboxed test in crates/directory/src, start a loopback axum fixture on 127.0.0.1: serving a minimal RSS document at /secret, and a second fixture answering 302 to it. Send a snapshot as an 'rw' token with the redirecting fixture's URL, run Directory::refresh on the due feed, and assert that GET /v1/feeds/{id} shows the fixture's RSS title. Repeat with the direct 127.0.0.1 URL. A fix that refuses loopback, private, link-local, ULA and unspecified addresses at connect time on every hop (custom reqwest DNS resolver plus an IP-literal check per hop) should make the same tests record a failure instead.", 'deployment': "The owner checks, from inside the ipx-directory container's network namespace and without sending feed URLs, which addresses are reachable (Tower's LAN 192.168.1.0/24, the ipx, ipx-web and postgres services on the compose network, 127.0.0.1) and whether any of them answers a GET with RSS/Atom or performs an action on GET. Also confirm which rw tokens exist in directory.toml and who holds them."}

Owner check (no probing). The owner checks, from inside the ipx-directory container's network namespace and without sending feed URLs, which addresses are reachable (Tower's LAN 192.168.1.0/24, the ipx, ipx-web and postgres services on the compose network, 127.0.0.1) and whether any of them answers a GET with RSS/Atom or performs an action on GET. Also confirm which rw tokens exist in directory.toml and who holds them.

Full detail: /config/security-audit-skill/ipx/run-1/NEEDS-VALIDATION.md (fingerprint directory-refresher-fetch-no-private-address-policy).

Found by the security-audit skill (run-1, 2026-10-20). Source-read only: nothing was run, so this is a lead that still needs its local check, not a confirmed finding. Two independent verifiers did not disprove it. Feed URLs reach the directory's refresher from rw-token snapshots (lib.rs snapshot, need(Access::Rw)) and from Apple's lookup feedUrl (apple.rs lookup_entries, publisher-influenced). Store::snapshot keeps any http(s) URL with a host that url_looks_private does not flag, and that function tests only paid hosts, userinfo and credential-like query keys, not destination addresses. Directory::refresh then calls ipx_feed::fetch, which follows up to ten Location headers to any host and connects with req.send() from the directory container's network. So an rw peer can submit http://192.168.1.x:port/... or a Docker service name directly, and the publisher of any already-listed feed can answer the refresher with a 30x to a loopback, LAN or compose-network address. If the target answers with RSS or Atom, its parsed title, description and items are saved (Store::save) and shown on the public pages and /v1; otherwise a listed feed failing for 24 hours shows only the coarse failure_words reason, a slow, low-resolution reachability oracle. Requests are unauthenticated GETs (fetch is called with auth None), so impact depends on what internal services answer to a plain GET. **Claimed root cause.** Store::normalise (store.rs:41) accepts any http(s) URL with a host, and the only refusal, ipx_feed::url_looks_private (feed/src/lib.rs:1274), is about credentials and paid hosts. ipx_feed::fetch (feed/src/lib.rs:136-189) follows up to ten redirects to any host, and the directory's reqwest client (lib.rs:137-143) has Policy::none only so fetch sees each hop, with no resolver or connector hook. No loopback, RFC 1918, link-local, ULA or internal-name policy is applied before connecting or after resolution on any hop. **Local check.** {'local': "In a sandboxed test in crates/directory/src, start a loopback axum fixture on 127.0.0.1:<port> serving a minimal RSS document at /secret, and a second fixture answering 302 to it. Send a snapshot as an 'rw' token with the redirecting fixture's URL, run Directory::refresh on the due feed, and assert that GET /v1/feeds/{id} shows the fixture's RSS title. Repeat with the direct 127.0.0.1 URL. A fix that refuses loopback, private, link-local, ULA and unspecified addresses at connect time on every hop (custom reqwest DNS resolver plus an IP-literal check per hop) should make the same tests record a failure instead.", 'deployment': "The owner checks, from inside the ipx-directory container's network namespace and without sending feed URLs, which addresses are reachable (Tower's LAN 192.168.1.0/24, the ipx, ipx-web and postgres services on the compose network, 127.0.0.1) and whether any of them answers a GET with RSS/Atom or performs an action on GET. Also confirm which rw tokens exist in directory.toml and who holds them."} **Owner check (no probing).** The owner checks, from inside the ipx-directory container's network namespace and without sending feed URLs, which addresses are reachable (Tower's LAN 192.168.1.0/24, the ipx, ipx-web and postgres services on the compose network, 127.0.0.1) and whether any of them answers a GET with RSS/Atom or performs an action on GET. Also confirm which rw tokens exist in directory.toml and who holds them. Full detail: `/config/security-audit-skill/ipx/run-1/NEEDS-VALIDATION.md` (fingerprint `directory-refresher-fetch-no-private-address-policy`).
rays added the bug label 2026-10-23 05:26:48 -07:00
Author
Owner

Fixed in 3bfd8f8: the refresher's client has ipx-feed's guard (#320) with no exceptions, so a feed named in a snapshot, or a redirect, to an internal address is refused before the request is sent. Deployed; the directory's pages answer and its log shows no refusals.

Fixed in 3bfd8f8: the refresher's client has ipx-feed's guard (#320) with no exceptions, so a feed named in a snapshot, or a redirect, to an internal address is refused before the request is sent. Deployed; the directory's pages answer and its log shows no refusals.
rays closed this issue 2026-10-23 07:14:20 -07:00
Author
Owner

Follow-up in 46fe0dd: directory.toml takes fetch_internal as config.toml does (empty by default, none in production). Without it the browser suite's directory could not refresh its fixture feeds on 127.0.0.1. Deployed.

Follow-up in 46fe0dd: directory.toml takes `fetch_internal` as config.toml does (empty by default, none in production). Without it the browser suite's directory could not refresh its fixture feeds on 127.0.0.1. Deployed.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: rays/ipx#329