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

@@ -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;