A multi-file torrent's name is recorded unchecked as the enclosure path, so a feed publisher can point Delete, reap and media serving at any path #318

Closed
opened 2026-10-23 05:26:41 -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.

When a torrent enclosure finishes, Torrents::fetch returns handle.output_folder().join(handle.name()) as the episode's path. The name is the torrent info dictionary's name (or the magnet's dn when info has none), which the feed publisher controls. Because ipx passes output_folder explicitly, librqbit 9.0.1 takes the (Some(o), None) branch and never runs get_default_subfolder_for_torrent, the only place it checks the name for non-Normal components; its metainfo validate() checks each file's path components (and, for a single-file torrent, the name itself, since the name is then the file name), but not a multi-file torrent's info.name. So a two-file torrent named '/config/config.toml', '/', '..' or '.' is accepted, its files land harmlessly as dest_dir/, and PathBuf::join with an absolute name discards dest_dir. The resulting path is written verbatim to enclosures.path by mark_downloaded (main.rs:1963, 2270, 2293, 2356). Afterwards: (1) the age/quota reaper (retention::remove) or any subscriber's Delete (web.rs delete_file) calls download::remove, which runs remove_dir_all on a directory, so '/', '..', the downloads root or /config could be deleted as far as the daemon can write; (2) every subscriber's GET /media/ serves ServeFile(download::playable(path)): the named file itself or the largest file under a named directory, and media_save reads the same path. A household user who subscribes to a feed they control (or any household subscribing to a malicious publisher's feed) can thus read config.toml (holding the shared web token, which maps to the first admin), a SQLite database, or other daemon-readable files, and the publisher alone can trigger deletion via the reaper. Torrents are enabled by default (config.rs:224).

Claimed root cause. torrent.rs:106-107 trusts librqbit's ManagedTorrent::name() (torrent_state/mod.rs:219-228 returns info.name or magnet dn unvalidated). librqbit 9.0.1 validates the name only when it derives a default subfolder (session.rs:1188-1201), which ipx bypasses by setting output_folder (torrent.rs:65; session.rs:1286-1291), and ipx does not require the joined path to be one Normal component under dest_dir. None of the sinks (download::remove, download::playable, ServeFile, deadair::kept) re-checks that the stored path is under download_dir.

Local check. {'local': "In a throwaway sandbox with loopback-only networking: create /tmp/ipx-v-target/ holding a dummy file secret.txt. Build a 2-file torrent (a.bin, b.bin, a few bytes each) whose info.name is '/tmp/ipx-v-target/secret.txt', and a second whose name is '/tmp/ipx-v-target'. Seed both from a local client on 127.0.0.1 with a local tracker. Run a scratch daemon with download_dir under /tmp/ipx-v-dl, a dummy feed on 127.0.0.1 serving the .torrent files as enclosures (type application/x-bittorrent), and one dummy subscriber. After completion, check enclosures.path equals the crafted names; GET /media/ as the subscriber returns secret.txt's bytes; DELETE the second download (force) and check /tmp/ipx-v-target is gone. Regression test: assert Torrents::fetch's returned path starts with dest_dir and has exactly one Normal component after it (or reject the torrent otherwise).", 'deployment': "Owner check, no probing: read-only query on production's database for any enclosures row whose path is outside the configured download dir (SELECT id, path FROM enclosures WHERE path IS NOT NULL AND path NOT LIKE '/downloads/%'), and confirm whether [torrent] enabled is true (default) in /mnt/fast/appdata/ipx/config.toml."}

Owner check (no probing). Owner check, no probing: read-only query on production's database for any enclosures row whose path is outside the configured download dir (SELECT id, path FROM enclosures WHERE path IS NOT NULL AND path NOT LIKE '/downloads/%'), and confirm whether [torrent] enabled is true (default) in /mnt/fast/appdata/ipx/config.toml.

Full detail: /config/security-audit-skill/ipx/run-1/NEEDS-VALIDATION.md (fingerprint ipx:torrent.rs:fetch:unvalidated-torrent-name-path).

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. When a torrent enclosure finishes, Torrents::fetch returns handle.output_folder().join(handle.name()) as the episode's path. The name is the torrent info dictionary's name (or the magnet's dn when info has none), which the feed publisher controls. Because ipx passes output_folder explicitly, librqbit 9.0.1 takes the (Some(o), None) branch and never runs get_default_subfolder_for_torrent, the only place it checks the name for non-Normal components; its metainfo validate() checks each file's path components (and, for a single-file torrent, the name itself, since the name is then the file name), but not a multi-file torrent's info.name. So a two-file torrent named '/config/config.toml', '/', '..' or '.' is accepted, its files land harmlessly as dest_dir/<file path>, and PathBuf::join with an absolute name discards dest_dir. The resulting path is written verbatim to enclosures.path by mark_downloaded (main.rs:1963, 2270, 2293, 2356). Afterwards: (1) the age/quota reaper (retention::remove) or any subscriber's Delete (web.rs delete_file) calls download::remove, which runs remove_dir_all on a directory, so '/', '..', the downloads root or /config could be deleted as far as the daemon can write; (2) every subscriber's GET /media/<id> serves ServeFile(download::playable(path)): the named file itself or the largest file under a named directory, and media_save reads the same path. A household user who subscribes to a feed they control (or any household subscribing to a malicious publisher's feed) can thus read config.toml (holding the shared web token, which maps to the first admin), a SQLite database, or other daemon-readable files, and the publisher alone can trigger deletion via the reaper. Torrents are enabled by default (config.rs:224). **Claimed root cause.** torrent.rs:106-107 trusts librqbit's ManagedTorrent::name() (torrent_state/mod.rs:219-228 returns info.name or magnet dn unvalidated). librqbit 9.0.1 validates the name only when it derives a default subfolder (session.rs:1188-1201), which ipx bypasses by setting output_folder (torrent.rs:65; session.rs:1286-1291), and ipx does not require the joined path to be one Normal component under dest_dir. None of the sinks (download::remove, download::playable, ServeFile, deadair::kept) re-checks that the stored path is under download_dir. **Local check.** {'local': "In a throwaway sandbox with loopback-only networking: create /tmp/ipx-v-target/ holding a dummy file secret.txt. Build a 2-file torrent (a.bin, b.bin, a few bytes each) whose info.name is '/tmp/ipx-v-target/secret.txt', and a second whose name is '/tmp/ipx-v-target'. Seed both from a local client on 127.0.0.1 with a local tracker. Run a scratch daemon with download_dir under /tmp/ipx-v-dl, a dummy feed on 127.0.0.1 serving the .torrent files as enclosures (type application/x-bittorrent), and one dummy subscriber. After completion, check enclosures.path equals the crafted names; GET /media/<id> as the subscriber returns secret.txt's bytes; DELETE the second download (force) and check /tmp/ipx-v-target is gone. Regression test: assert Torrents::fetch's returned path starts with dest_dir and has exactly one Normal component after it (or reject the torrent otherwise).", 'deployment': "Owner check, no probing: read-only query on production's database for any enclosures row whose path is outside the configured download dir (SELECT id, path FROM enclosures WHERE path IS NOT NULL AND path NOT LIKE '/downloads/%'), and confirm whether [torrent] enabled is true (default) in /mnt/fast/appdata/ipx/config.toml."} **Owner check (no probing).** Owner check, no probing: read-only query on production's database for any enclosures row whose path is outside the configured download dir (SELECT id, path FROM enclosures WHERE path IS NOT NULL AND path NOT LIKE '/downloads/%'), and confirm whether [torrent] enabled is true (default) in /mnt/fast/appdata/ipx/config.toml. Full detail: `/config/security-audit-skill/ipx/run-1/NEEDS-VALIDATION.md` (fingerprint `ipx:torrent.rs:fetch:unvalidated-torrent-name-path`).
rays added the bug label 2026-10-23 05:26:41 -07:00
Author
Owner

Fixed in ec3dedf: each torrent downloads into torrent-<enclosure id> inside its feed's folder, and that folder is the stored path. The torrent's own name is no longer used for any path. Deployed to production (ipx -dev).

Fixed in ec3dedf: each torrent downloads into `torrent-<enclosure id>` inside its feed's folder, and that folder is the stored path. The torrent's own name is no longer used for any path. Deployed to production (ipx -dev).
rays closed this issue 2026-10-23 05:56:57 -07:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: rays/ipx#318