Ask for a post's images without a referrer
jeffgeerling.com answers 403 to an image request whose Referer is another site, so his posts showed a broken image on iOS and the alt text on desktop. The sanitiser now gives every <img> referrerpolicy="no-referrer". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -42,6 +42,8 @@ The long form, with what was wrong before and how it was found, is in
|
|||||||
their own, and on white for an iPhone's home screen.
|
their own, and on white for an iPhone's home screen.
|
||||||
- The file icon of an item not yet downloaded sits level with the rest of its row, instead of
|
- The file icon of an item not yet downloaded sits level with the rest of its row, instead of
|
||||||
higher than a downloaded one's.
|
higher than a downloaded one's.
|
||||||
|
- Images in posts from sites that refuse images to other sites' pages, such as Jeff Geerling's,
|
||||||
|
now show: ipx asks for them without saying it is the page showing them.
|
||||||
- An item you open stays read. A list refresh that crossed with marking it read could put its
|
- An item you open stays read. A list refresh that crossed with marking it read could put its
|
||||||
unread dot back until the next refresh.
|
unread dot back until the next refresh.
|
||||||
- On the Unread tab, the item you were reading leaves the list as soon as you move to the next
|
- On the Unread tab, the item you were reading leaves the list as soon as you move to the next
|
||||||
|
|||||||
22
src/web.rs
22
src/web.rs
@@ -858,12 +858,13 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_relative_image_resolves_against_the_post() {
|
fn a_relative_image_resolves_against_the_post() {
|
||||||
let mut b = ammonia::Builder::new();
|
let mut b = feed_sanitizer();
|
||||||
let html = r#"<img src="images/a.webp"><a href="/about">x</a><script>bad()</script>"#;
|
let html = r#"<img src="images/a.webp"><a href="/about">x</a><script>bad()</script>"#;
|
||||||
let out = clean_description(&mut b, html, Some("https://example.com/2026/04/post/"));
|
let out = clean_description(&mut b, html, Some("https://example.com/2026/04/post/"));
|
||||||
assert!(out.contains(r#"src="https://example.com/2026/04/post/images/a.webp""#), "{out}");
|
assert!(out.contains(r#"src="https://example.com/2026/04/post/images/a.webp""#), "{out}");
|
||||||
assert!(out.contains(r#"href="https://example.com/about""#), "{out}");
|
assert!(out.contains(r#"href="https://example.com/about""#), "{out}");
|
||||||
assert!(!out.contains("bad()"), "{out}");
|
assert!(!out.contains("bad()"), "{out}");
|
||||||
|
assert!(out.contains(r#"referrerpolicy="no-referrer""#), "{out}");
|
||||||
assert!(clean_description(&mut b, html, None).contains(r#"src="images/a.webp""#));
|
assert!(clean_description(&mut b, html, None).contains(r#"src="images/a.webp""#));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1046,11 +1047,7 @@ fn entry_page(
|
|||||||
);
|
);
|
||||||
let mut rows =
|
let mut rows =
|
||||||
db.entries_in(user_id, feed, filter, search, page.offset, page.limit.clamp(1, 200), &order)?;
|
db.entries_in(user_id, feed, filter, search, page.offset, page.limit.clamp(1, 200), &order)?;
|
||||||
// Feed HTML is untrusted: it reaches the page only after ammonia has been through it.
|
let mut sanitizer = feed_sanitizer();
|
||||||
// Every link opens in a new tab -- ammonia's default rel="noopener noreferrer" already
|
|
||||||
// keeps that safe -- so following one in show notes never navigates away from ipx.
|
|
||||||
let mut sanitizer = ammonia::Builder::new();
|
|
||||||
sanitizer.add_tag_attributes("a", &["target"]).set_tag_attribute_value("a", "target", "_blank");
|
|
||||||
for row in &mut rows {
|
for row in &mut rows {
|
||||||
if let Some(d) = &row.description {
|
if let Some(d) = &row.description {
|
||||||
row.description = Some(clean_description(&mut sanitizer, d, row.link.as_deref()));
|
row.description = Some(clean_description(&mut sanitizer, d, row.link.as_deref()));
|
||||||
@@ -1060,6 +1057,19 @@ fn entry_page(
|
|||||||
Ok(Json(EntryPage { total, entries: rows }))
|
Ok(Json(EntryPage { total, entries: rows }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Feed HTML is untrusted: it reaches the page only after ammonia has been through it.
|
||||||
|
fn feed_sanitizer() -> ammonia::Builder<'static> {
|
||||||
|
let mut b = ammonia::Builder::new();
|
||||||
|
// Every link opens in a new tab -- ammonia's default rel="noopener noreferrer" already
|
||||||
|
// keeps that safe -- so following one in show notes never navigates away from ipx.
|
||||||
|
b.add_tag_attributes("a", &["target"]).set_tag_attribute_value("a", "target", "_blank");
|
||||||
|
// An image is asked for without saying it is shown on ipx. A site that refuses images to
|
||||||
|
// other sites' pages goes by that: jeffgeerling.com answers 403, and his posts showed a
|
||||||
|
// broken image on iOS and only the alt text on desktop.
|
||||||
|
b.add_tag_attributes("img", &["referrerpolicy"]).set_tag_attribute_value("img", "referrerpolicy", "no-referrer");
|
||||||
|
b
|
||||||
|
}
|
||||||
|
|
||||||
/// A relative `src` or `href` in a post means relative to the post, not to ipx: The
|
/// A relative `src` or `href` in a post means relative to the post, not to ipx: The
|
||||||
/// Observation Deck's `images/37k-a-day-bro.webp` came up as a broken image.
|
/// Observation Deck's `images/37k-a-day-bro.webp` came up as a broken image.
|
||||||
fn clean_description(sanitizer: &mut ammonia::Builder, html: &str, link: Option<&str>) -> String {
|
fn clean_description(sanitizer: &mut ammonia::Builder, html: &str, link: Option<&str>) -> String {
|
||||||
|
|||||||
Reference in New Issue
Block a user