From 3bfd8f848b2f8dddcd5c4a04b205630df25d4a3b Mon Sep 17 00:00:00 2001 From: rays Date: Fri, 23 Oct 2026 14:10:32 +0000 Subject: [PATCH] The directory's refresher refuses internal addresses (#329) The refresher fetches every feed a snapshot names, and anyone with a write token sends snapshots; a listed feed's redirect goes wherever its publisher likes. Nothing checked the destination, so the directory fetched loopback, its Docker network and the LAN, and what answered became a listing's title and description on its public pages. Its client now has ipx-feed's guard (#320) with nothing allowed: no feed the directory lists is on this network. Co-Authored-By: Claude Opus 5.5 --- crates/directory/CHANGELOG.md | 1 + crates/directory/src/lib.rs | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/crates/directory/CHANGELOG.md b/crates/directory/CHANGELOG.md index 77d8540..bc51bc7 100644 --- a/crates/directory/CHANGELOG.md +++ b/crates/directory/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - HTTPS connections no longer trust certificates from emSign's distrusted intermediates, and certificate checks use constant-time comparisons: rustls-webpki, webpki-roots and ctutils are updated (#351). - The public pages read the list from the database at most once a minute, however many people ask. Each page asked for the whole catalogue, so a flood of requests with query strings of their own reached the database every time (#341). +- The directory no longer fetches feeds at internal addresses (loopback, the LAN, Docker's networks), named in a snapshot or reached by a redirect. It fetched them, and stored what answered as the feed's title and description (#329). ## [1.3.0] - 2026-10-19 diff --git a/crates/directory/src/lib.rs b/crates/directory/src/lib.rs index 424534a..017bbad 100644 --- a/crates/directory/src/lib.rs +++ b/crates/directory/src/lib.rs @@ -144,6 +144,9 @@ impl Directory { .read_timeout(ipx_feed::FEED_SILENCE) .redirect(reqwest::redirect::Policy::none()) .user_agent(concat!("ipx-directory/", env!("CARGO_PKG_VERSION"))) + // The feeds are named by whoever holds a write token, and redirect where their + // publishers like; none is on this network, so nothing here is fetched (#329). + .connector_layer(ipx_feed::guard::Guard::new(Default::default())) .build()?; Ok(Arc::new(Self { store, @@ -568,6 +571,16 @@ mod tests { assert_eq!(got["accepted"], 1); } + #[tokio::test] + async fn the_refresher_fetches_nothing_on_this_network() { + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + tokio::spawn(async move { while listener.accept().await.is_ok() {} }); + let (_, dir) = app().await; + let err = dir.client.get(format!("http://{addr}/feed.xml")).send().await.unwrap_err(); + assert!(format!("{err:?}").contains("internal address"), "{err:?}"); + } + #[tokio::test] async fn the_public_pages_read_the_list_once_a_minute() { let (_, dir) = app().await;