One rw snapshot with an enormous subscriber count breaks the directory's listings for everyone #342
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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.
A holder of an rw directory token (another iPX server) can send a snapshot whose entry has subscribers = 9223372036854775807 (i64::MAX) for a feed that at least one other instance also sends with a count of 1 or more. The attacker can find such feeds through /v1/feeds, which any ro token reads. The listing query sums the counts of every instance per feed. On Postgres, sum(bigint) gives a numeric and the CAST to BIGINT fails with 'bigint out of range'. On SQLite, sum() over integers raises 'integer overflow'. Both list() and popular() run this aggregate over every feed, so the source predicts they fail for every caller. The public pages /, /all, /search and /category would answer 503 through failed(). /v1/feeds and /v1/popular would answer 500 to every ipx server, and so would that feed's /v1/feeds/{id} and /feed/{id}. This lasts until an rwa admin drops the instance or its token is revoked. Apple's chart rows carry 0 subscribers (apple.rs:223), so they cannot supply the second row. The attacker needs a feed that a real peer instance also sends. Not executed in this run.
Claimed root cause. Store::snapshot clamps a submitted subscriber count only from below (
e.subscribers.max(0), store.rs:224) and stores any i64. Store::LISTING then computes CAST(sum(s.subscribers) AS BIGINT) across all instances' rows for a feed (store.rs:291), and popular() also orders by sum(s.subscribers) (store.rs:319), with nothing bounding the inputs. So two instances' values can add up past the 64-bit range and the query errors.Local check. {'local': 'Inside a sandbox, add a test to crates/directory/src/lib.rs tests using app() with a second rw token configured. POST /v1/snapshot as the first rw token with [{"url":"https://x.test/a.xml","subscribers":1}]. POST as the second rw token with the same URL and "subscribers":9223372036854775807. Mark the feed fetched (set items non-null, for example with the store tests' fetch_all helper on a fixture feed). Expect GET /v1/feeds and /v1/popular as ro to return 500, and GET / and /all to return 503. Run cargo test -p ipx-directory on SQLite, then again with IPX_TEST_DATABASE_URL pointed at a _test Postgres database. Fix: in Store::snapshot, clamp to a sane ceiling (for example e.subscribers.clamp(0, 1_000_000)), and optionally reject over-range values in the handler. With the fix, the test expects 200 and a bounded sum.', 'deployment': 'Owner-observed and non-destructive: do not send the overflowing snapshot to production. Instead, run SELECT max(subscribers) FROM snapshots on ipx_directory read-only, to confirm no stored value is anywhere near the i64 range. Then check that the fix is deployed by confirming the clamp in the running ipx-directory version.'}
Owner check (no probing). Owner-observed and non-destructive: do not send the overflowing snapshot to production. Instead, run SELECT max(subscribers) FROM snapshots on ipx_directory read-only, to confirm no stored value is anywhere near the i64 range. Then check that the fix is deployed by confirming the clamp in the running ipx-directory version.
Full detail:
/config/security-audit-skill/ipx/run-1/NEEDS-VALIDATION.md(fingerprintdirectory-snapshot-subscribers-unbounded-sum-overflow).Fixed in
543d35f: Store::snapshot clamps each count to 0..=1,000,000, so sums across instances stay in range. A test sends i64::MAX from two instances and lists, popular and instances still answer (run on Postgres). Deployed.