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:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user