One feed listing many distinct http artwork hosts holds the shared scan worker for hours, on every scan #339

Closed
opened 2026-10-23 05:26:54 -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 user can add a feed URL (add_feed, then scan_soon queues a forced Fetch). On each full read of that feed, scan_one passes the feed's artwork and every entry's artwork through feed::prefer_https one at a time, awaited inside the daemon's single worker loop. The per-scan cache is keyed by the raw authority (everything before the first '/', so host:port and any userinfo), so the publisher decides how many distinct keys there are: http://x.evil:1/a.png, http://x.evil:2/a.png, ... or a@x.evil, b@x.evil, all reaching one server. If that server accepts each TLS connection and then says nothing, each probe costs the full 20 s is_image timeout. Probes to closed ports fail fast, so the attacker needs a server that accepts and stays silent. Images that stay on http are stored, and http_images() returns them on every later full read, where they are probed again. A timed scan is not forced, so the publisher has to change the body each time to get past the 304 and body-hash shortcuts. Nothing in the source puts a deadline on scan_one or the worker job: until_stopped only races the job against shutdown. While the worker is busy, no other feed is scanned or downloaded for any user, and queued Fetch commands wait.

Claimed root cause. feed::prefer_https and is_image run once per distinct authority. Nothing caps how many distinct hosts one feed can cause to be probed, and scan_one has no overall deadline. The cache key is the unparsed authority (crates/feed/src/lib.rs:586), so the publisher chooses how many keys there are. The probes run one after another (main.rs:1795-1799, 1833-1835) inside the single daemon worker loop (crates/ipx/src/main.rs:707-722), whose job is raced only against shutdown (main.rs:684-697).

Local check. {'local': 'In the sandbox, add a scan_one test beside the existing fixture-server tests in crates/ipx/src/main.rs. Serve one RSS feed with 50 items whose itunes:image values are http://127.0.0.1:

/a.png for 50 different loopback ports, or 50 userinfo variants of one port. Behind each one, run a TCP listener that accepts and never answers. Wrap scan_one in tokio::time::timeout with a budget of a few seconds, and count the connections the listeners accept. Expected now: 50 sequential accepts, and scan time that grows linearly with the item count (each probe at about 20 s if run to completion). Run it again with a second feed queued, to show it is not scanned until the first finishes. After a fix: at most K probes per scan, and the scan finishes within its deadline.', 'deployment': 'Owner check, non-destructive: confirm that production runs one daemon with one worker, that household non-admin accounts can add arbitrary feed URLs, and whether a proxy or egress policy restricts outbound HTTPS from the iPX container to arbitrary hosts and ports. Do not test it by adding a hostile feed in production.'}

Owner check (no probing). Owner check, non-destructive: confirm that production runs one daemon with one worker, that household non-admin accounts can add arbitrary feed URLs, and whether a proxy or egress policy restricts outbound HTTPS from the iPX container to arbitrary hosts and ports. Do not test it by adding a hostile feed in production.

Full detail: /config/security-audit-skill/ipx/run-1/NEEDS-VALIDATION.md (fingerprint ipx:feed:prefer_https-per-host-unbounded-sequential-probes).

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 user can add a feed URL (add_feed, then scan_soon queues a forced Fetch). On each full read of that feed, scan_one passes the feed's artwork and every entry's artwork through feed::prefer_https one at a time, awaited inside the daemon's single worker loop. The per-scan cache is keyed by the raw authority (everything before the first '/', so host:port and any userinfo), so the publisher decides how many distinct keys there are: http://x.evil:1/a.png, http://x.evil:2/a.png, ... or a@x.evil, b@x.evil, all reaching one server. If that server accepts each TLS connection and then says nothing, each probe costs the full 20 s is_image timeout. Probes to closed ports fail fast, so the attacker needs a server that accepts and stays silent. Images that stay on http are stored, and http_images() returns them on every later full read, where they are probed again. A timed scan is not forced, so the publisher has to change the body each time to get past the 304 and body-hash shortcuts. Nothing in the source puts a deadline on scan_one or the worker job: until_stopped only races the job against shutdown. While the worker is busy, no other feed is scanned or downloaded for any user, and queued Fetch commands wait. **Claimed root cause.** feed::prefer_https and is_image run once per distinct authority. Nothing caps how many distinct hosts one feed can cause to be probed, and scan_one has no overall deadline. The cache key is the unparsed authority (crates/feed/src/lib.rs:586), so the publisher chooses how many keys there are. The probes run one after another (main.rs:1795-1799, 1833-1835) inside the single daemon worker loop (crates/ipx/src/main.rs:707-722), whose job is raced only against shutdown (main.rs:684-697). **Local check.** {'local': 'In the sandbox, add a scan_one test beside the existing fixture-server tests in crates/ipx/src/main.rs. Serve one RSS feed with 50 items whose itunes:image values are http://127.0.0.1:<p>/a.png for 50 different loopback ports, or 50 userinfo variants of one port. Behind each one, run a TCP listener that accepts and never answers. Wrap scan_one in tokio::time::timeout with a budget of a few seconds, and count the connections the listeners accept. Expected now: 50 sequential accepts, and scan time that grows linearly with the item count (each probe at about 20 s if run to completion). Run it again with a second feed queued, to show it is not scanned until the first finishes. After a fix: at most K probes per scan, and the scan finishes within its deadline.', 'deployment': 'Owner check, non-destructive: confirm that production runs one daemon with one worker, that household non-admin accounts can add arbitrary feed URLs, and whether a proxy or egress policy restricts outbound HTTPS from the iPX container to arbitrary hosts and ports. Do not test it by adding a hostile feed in production.'} **Owner check (no probing).** Owner check, non-destructive: confirm that production runs one daemon with one worker, that household non-admin accounts can add arbitrary feed URLs, and whether a proxy or egress policy restricts outbound HTTPS from the iPX container to arbitrary hosts and ports. Do not test it by adding a hostile feed in production. Full detail: `/config/security-audit-skill/ipx/run-1/NEEDS-VALIDATION.md` (fingerprint `ipx:feed:prefer_https-per-host-unbounded-sequential-probes`).
rays added the bug label 2026-10-23 05:26:54 -07:00
Author
Owner

Fixed in d35b805: prefer_https asks at most 8 hosts per read of a feed; past that an http image stays http (for /api/art), as one that fails does. Deployed.

Fixed in d35b805: prefer_https asks at most 8 hosts per read of a feed; past that an http image stays http (for /api/art), as one that fails does. Deployed.
rays closed this issue 2026-10-23 07:20:00 -07:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: rays/ipx#339