fix: drop unsafe upstream media entries

This commit is contained in:
2026-09-24 19:39:46 +08:00
parent 8428468712
commit f08b187964
2 changed files with 7 additions and 24 deletions
+1 -1
View File
@@ -112,7 +112,7 @@ async fn refetch(
let items: Vec<MediaItemPayload> = fetched let items: Vec<MediaItemPayload> = fetched
.media .media
.iter() .iter()
.map(|media| media_to_payload(media, fetched.sensitive)) .filter_map(|media| media_to_payload(media, fetched.sensitive))
.collect(); .collect();
// The re-fetch may produce a fresh local file (ugoira / bsky remux): hand it // The re-fetch may produce a fresh local file (ugoira / bsky remux): hand it
// to the same keep-alive registry the first fetch uses. // to the same keep-alive registry the first fetch uses.
+6 -23
View File
@@ -182,7 +182,7 @@ pub(super) fn cached_snapshot(fetched: &x_media::site::Fetched) -> Option<Cached
}) })
} }
pub(super) fn media_to_payload(media: &Media, sensitive: bool) -> MediaItemPayload { pub(super) fn media_to_payload(media: &Media, sensitive: bool) -> Option<MediaItemPayload> {
let url = media.url(); let url = media.url();
// Only adapter-produced temp files may be non-HTTP. A bare path from an // Only adapter-produced temp files may be non-HTTP. A bare path from an
// upstream JSON field must never reach InputFile::file(). // upstream JSON field must never reach InputFile::file().
@@ -192,18 +192,14 @@ pub(super) fn media_to_payload(media: &Media, sensitive: bool) -> MediaItemPaylo
.is_some_and(|name| name.starts_with(x_media::TEMP_FILE_PREFIX)); .is_some_and(|name| name.starts_with(x_media::TEMP_FILE_PREFIX));
if !url.starts_with("http://") && !url.starts_with("https://") && !is_local_temp { if !url.starts_with("http://") && !url.starts_with("https://") && !is_local_temp {
log::warn!("dropping media with a non-URL upstream path"); log::warn!("dropping media with a non-URL upstream path");
return MediaItemPayload::Photo { return None;
media: MediaRef::Source(String::new()),
has_spoiler: sensitive,
fallback_url: None,
};
} }
let media_ref = MediaRef::Source(url.to_string()); let media_ref = MediaRef::Source(url.to_string());
let fallback_url = media let fallback_url = media
.smaller_url() .smaller_url()
.filter(|url| url.starts_with("http://") || url.starts_with("https://")) .filter(|url| url.starts_with("http://") || url.starts_with("https://"))
.map(str::to_string); .map(str::to_string);
match media { Some(match media {
// A gif inside a group becomes a video item; a lone gif takes the // A gif inside a group becomes a video item; a lone gif takes the
// animation path (see url_media). // animation path (see url_media).
Media::Illustration { .. } => MediaItemPayload::Photo { Media::Illustration { .. } => MediaItemPayload::Photo {
@@ -223,7 +219,7 @@ pub(super) fn media_to_payload(media: &Media, sensitive: bool) -> MediaItemPaylo
thumbnail: thumbnail_for(media), thumbnail: thumbnail_for(media),
fallback_url, fallback_url,
}, },
} })
} }
/// Sends a task and handles the outcome: post-send actions on success, retry /// Sends a task and handles the outcome: post-send actions on success, retry
@@ -662,11 +658,8 @@ async fn url_media_inner(
let items: Vec<MediaItemPayload> = fetched let items: Vec<MediaItemPayload> = fetched
.media .media
.iter() .iter()
.map(|media| media_to_payload(media, fetched.sensitive)) .filter_map(|media| media_to_payload(media, fetched.sensitive))
.collect(); .collect();
// The indicator switches to "sending photo/video" once the kinds
// are known; `items` is moved into the task below.
*hint.lock() = ActionHint::for_items(&items);
let task = build_send_task( let task = build_send_task(
&chat_data, &chat_data,
chat_id, chat_id,
@@ -825,17 +818,7 @@ mod tests {
thumbnail_url: Some("/etc/passwd".into()), thumbnail_url: Some("/etc/passwd".into()),
fallback_url: Some("https://safe.example/fallback.jpg".into()), fallback_url: Some("https://safe.example/fallback.jpg".into()),
}; };
match media_to_payload(&media, false) { assert!(media_to_payload(&media, false).is_none());
MediaItemPayload::Photo {
media: MediaRef::Source(source),
fallback_url,
..
} => {
assert!(source.is_empty());
assert_eq!(fallback_url.as_deref(), None);
}
other => panic!("expected rejected photo payload, got {other:?}"),
}
} }
/// The caption-quote threshold matches the post's text inside the caption, /// The caption-quote threshold matches the post's text inside the caption,