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 <noreply@anthropic.com>
This commit is contained in:
2026-10-23 14:10:32 +00:00
parent fc04a741a6
commit 3bfd8f848b
2 changed files with 14 additions and 0 deletions

View File

@@ -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). - 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 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 ## [1.3.0] - 2026-10-19

View File

@@ -144,6 +144,9 @@ impl Directory {
.read_timeout(ipx_feed::FEED_SILENCE) .read_timeout(ipx_feed::FEED_SILENCE)
.redirect(reqwest::redirect::Policy::none()) .redirect(reqwest::redirect::Policy::none())
.user_agent(concat!("ipx-directory/", env!("CARGO_PKG_VERSION"))) .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()?; .build()?;
Ok(Arc::new(Self { Ok(Arc::new(Self {
store, store,
@@ -568,6 +571,16 @@ mod tests {
assert_eq!(got["accepted"], 1); 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] #[tokio::test]
async fn the_public_pages_read_the_list_once_a_minute() { async fn the_public_pages_read_the_list_once_a_minute() {
let (_, dir) = app().await; let (_, dir) = app().await;