Feed, enclosure, OPML and artwork fetches reach any internal address, and the responses come back to the user #320

Closed
opened 2026-10-23 05:26:43 -07:00 by rays · 1 comment
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.

Any signed-in household user, admin or not, and any publisher of a subscribed feed can make the ipx daemon send GET requests to loopback, the container network or the LAN behind it. ipx's outbound HTTP clients have no destination policy, and redirects are followed to any host. The responses come back three ways. (1) POST /api/feeds returns, in its 400 body, the HTTP status of a non-2xx answer, or for a 2xx non-markup body the first line of up to 80 characters (from parse's plain_text, via find_feed's 'is not a feed' error). (2) An enclosure in a feed that declares an audio or video MIME is downloaded whole from whatever URL it names, internal ones included. Only bodies starting with <html or <!doctype html are refused, so JSON or plain text is stored and served through /media/{id} to anyone subscribed to that feed. (3) /api/art fetches any image URL a feed in the database names, a value the feed's author controls, and its 502 error returns the status or content type: a status oracle, and a full read of internal images. No crate in the repository filters destination IPs. The directory crate's snapshot handling (store.rs) refuses only URLs that url_looks_private flags, and url_looks_private only spots paid hosts, credentials and key-like query names in a URL. It is never called before an ipx fetch.

Claimed root cause. The reqwest clients built in Ctx (crates/ipx/src/main.rs:358-371) have no resolver or connector filter. ctx.client also keeps reqwest's default redirect policy (any host, up to 10 hops), and feed::fetch's manual redirect loop (crates/feed/src/lib.rs:145-170) checks the host only to decide whether to send credentials. No caller (find_feed, feed::fetch, download::download, the /api/art handler, or the scan of OPML-imported URLs) checks for loopback, link-local, RFC1918, ULA or the host's own addresses, before the first request or after a redirect.

Local check. {'local': 'In a sandbox with no external network: start fixture HTTP server A on 127.0.0.1: serving GET /secret -> 200 application/json '{"k":"SECRET-0123"}', and fixture B serving an RSS feed whose single item has , plus GET /r -> 302 Location http://127.0.0.1:/secret. Use the browser-suite daemon setup under /tmp with a dummy non-admin user. (a) POST /api/feeds {"url":"http://127.0.0.1:/secret"} and assert the 400 body contains SECRET-0123. (b) Repeat with the url pointing at B's /r and assert the same through the redirect. (c) Add B's feed, wait for 'scan complete', and assert GET /media/ returns the JSON. Regression test for the fix: the same inputs are refused with an 'internal address' error and fixture A logs no connection.', 'deployment': "Owner check, no probing: list the unauthenticated HTTP services the ipx container can reach (its loopback, its Docker network, the LAN and the host's published ports) to judge the real exposure, and decide whether any household account or subscribed publisher is untrusted enough to matter."}

Owner check (no probing). Owner check, no probing: list the unauthenticated HTTP services the ipx container can reach (its loopback, its Docker network, the LAN and the host's published ports) to judge the real exposure, and decide whether any household account or subscribed publisher is untrusted enough to matter.

Full detail: /config/security-audit-skill/ipx/run-1/NEEDS-VALIDATION.md (fingerprint crates/ipx/src/main.rs:Ctx.client:no-outbound-destination-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. Any signed-in household user, admin or not, and any publisher of a subscribed feed can make the ipx daemon send GET requests to loopback, the container network or the LAN behind it. ipx's outbound HTTP clients have no destination policy, and redirects are followed to any host. The responses come back three ways. (1) POST /api/feeds returns, in its 400 body, the HTTP status of a non-2xx answer, or for a 2xx non-markup body the first line of up to 80 characters (from parse's plain_text, via find_feed's 'is not a feed' error). (2) An enclosure in a feed that declares an audio or video MIME is downloaded whole from whatever URL it names, internal ones included. Only bodies starting with <html or <!doctype html are refused, so JSON or plain text is stored and served through /media/{id} to anyone subscribed to that feed. (3) /api/art fetches any image URL a feed in the database names, a value the feed's author controls, and its 502 error returns the status or content type: a status oracle, and a full read of internal images. No crate in the repository filters destination IPs. The directory crate's snapshot handling (store.rs) refuses only URLs that url_looks_private flags, and url_looks_private only spots paid hosts, credentials and key-like query names in a URL. It is never called before an ipx fetch. **Claimed root cause.** The reqwest clients built in Ctx (crates/ipx/src/main.rs:358-371) have no resolver or connector filter. ctx.client also keeps reqwest's default redirect policy (any host, up to 10 hops), and feed::fetch's manual redirect loop (crates/feed/src/lib.rs:145-170) checks the host only to decide whether to send credentials. No caller (find_feed, feed::fetch, download::download, the /api/art handler, or the scan of OPML-imported URLs) checks for loopback, link-local, RFC1918, ULA or the host's own addresses, before the first request or after a redirect. **Local check.** {'local': 'In a sandbox with no external network: start fixture HTTP server A on 127.0.0.1:<p1> serving GET /secret -> 200 application/json \'{"k":"SECRET-0123"}\', and fixture B serving an RSS feed whose single item has <enclosure url="http://127.0.0.1:<p1>/secret" type="audio/mpeg"/>, plus GET /r -> 302 Location http://127.0.0.1:<p1>/secret. Use the browser-suite daemon setup under /tmp with a dummy non-admin user. (a) POST /api/feeds {"url":"http://127.0.0.1:<p1>/secret"} and assert the 400 body contains SECRET-0123. (b) Repeat with the url pointing at B\'s /r and assert the same through the redirect. (c) Add B\'s feed, wait for \'scan complete\', and assert GET /media/<enclosure id> returns the JSON. Regression test for the fix: the same inputs are refused with an \'internal address\' error and fixture A logs no connection.', 'deployment': "Owner check, no probing: list the unauthenticated HTTP services the ipx container can reach (its loopback, its Docker network, the LAN and the host's published ports) to judge the real exposure, and decide whether any household account or subscribed publisher is untrusted enough to matter."} **Owner check (no probing).** Owner check, no probing: list the unauthenticated HTTP services the ipx container can reach (its loopback, its Docker network, the LAN and the host's published ports) to judge the real exposure, and decide whether any household account or subscribed publisher is untrusted enough to matter. Full detail: `/config/security-audit-skill/ipx/run-1/NEEDS-VALIDATION.md` (fingerprint `crates/ipx/src/main.rs:Ctx.client:no-outbound-destination-policy`).
rays added the bug label 2026-10-23 05:26:43 -07:00
Author
Owner

Fixed in a20062d: a connector layer in ipx-feed (guard.rs) checks each connection's peer address before the request is written and refuses anything off the public internet unless config.toml's [general] fetch_internal lists it (addresses or networks). It is on ctx.client and feed_client, so it covers adding a feed, scans, downloads, OPML children, artwork and .torrent files, every redirect hop, and IP literals. The directory moved to an unguarded local_client. Deployed; on production, adding http://127.0.0.1:8099/ or http://ipx-directory:8100/ gets 400 '... is an internal address', the Directory still loads, and a forced check of the-verge fetched new items over HTTPS. librqbit's own tracker and peer connections are not covered.

Fixed in a20062d: a connector layer in ipx-feed (`guard.rs`) checks each connection's peer address before the request is written and refuses anything off the public internet unless config.toml's `[general] fetch_internal` lists it (addresses or networks). It is on ctx.client and feed_client, so it covers adding a feed, scans, downloads, OPML children, artwork and .torrent files, every redirect hop, and IP literals. The directory moved to an unguarded `local_client`. Deployed; on production, adding http://127.0.0.1:8099/ or http://ipx-directory:8100/ gets 400 '... is an internal address', the Directory still loads, and a forced check of the-verge fetched new items over HTTPS. librqbit's own tracker and peer connections are not covered.
rays closed this issue 2026-10-23 06:32:29 -07:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: rays/ipx#320