feat: detect webxdc by application/webxdc+zip MIME type

This is simpler and can count as hardening
because we don't call async_zip parser anymore
just to check if received file is a webxdc.
This way if ZIP parser is buggy and e.g. panics,
or gets into infinite loop, or is accidentally quadratic,
or allocates a lot of memory, this cannot be triggered
by just sending a file.
This commit is contained in:
link2xt
2026-10-13 03:26:50 +00:00
parent d766641ce2
commit 4e2ee61478
5 changed files with 61 additions and 128 deletions
+57 -60
View File
@@ -1509,73 +1509,70 @@ impl MimeMessage {
return Ok(());
}
let mut part = Part::default();
let msg_type = if context
.is_webxdc_file(filename, decoded_data)
.await
.unwrap_or(false)
{
Viewtype::Webxdc
} else if filename.ends_with(".kml") {
// XXX what if somebody sends eg an "location-highlights.kml"
// attachment unrelated to location streaming?
if filename.starts_with("location") || filename.starts_with("message") {
let parsed = location::Kml::parse(decoded_data)
let msg_type =
if filename.ends_with(".xdc") && mime_type.essence_str() == "application/webxdc+zip" {
Viewtype::Webxdc
} else if filename.ends_with(".kml") {
// XXX what if somebody sends eg an "location-highlights.kml"
// attachment unrelated to location streaming?
if filename.starts_with("location") || filename.starts_with("message") {
let parsed = location::Kml::parse(decoded_data)
.map_err(|err| {
warn!(context, "failed to parse kml part: {:#}", err);
})
.ok();
if filename.starts_with("location") {
self.location_kml = parsed;
} else {
self.message_kml = parsed;
}
return Ok(());
}
msg_type
} else if filename == "multi-device-sync.json" {
if !context.get_config_bool(Config::SyncMsgs).await? {
return Ok(());
}
let serialized = String::from_utf8_lossy(decoded_data)
.parse()
.unwrap_or_default();
self.sync_items = context
.parse_sync_items(serialized)
.map_err(|err| {
warn!(context, "failed to parse kml part: {:#}", err);
warn!(context, "failed to parse sync data: {:#}", err);
})
.ok();
if filename.starts_with("location") {
self.location_kml = parsed;
} else {
self.message_kml = parsed;
}
return Ok(());
}
msg_type
} else if filename == "multi-device-sync.json" {
if !context.get_config_bool(Config::SyncMsgs).await? {
} else if filename == "status-update.json" {
let serialized = String::from_utf8_lossy(decoded_data)
.parse()
.unwrap_or_default();
self.webxdc_status_update = Some(serialized);
return Ok(());
}
let serialized = String::from_utf8_lossy(decoded_data)
.parse()
.unwrap_or_default();
self.sync_items = context
.parse_sync_items(serialized)
.map_err(|err| {
warn!(context, "failed to parse sync data: {:#}", err);
})
.ok();
return Ok(());
} else if filename == "status-update.json" {
let serialized = String::from_utf8_lossy(decoded_data)
.parse()
.unwrap_or_default();
self.webxdc_status_update = Some(serialized);
return Ok(());
} else if msg_type == Viewtype::Vcard {
if let Some(summary) = get_vcard_summary(decoded_data) {
part.param.set(Param::Summary1, summary);
msg_type
} else {
Viewtype::File
}
} else if msg_type == Viewtype::Image
|| msg_type == Viewtype::Gif
|| msg_type == Viewtype::Sticker
{
match get_filemeta(decoded_data) {
// image size is known, not too big, keep msg_type:
Ok((width, height)) if width * height <= constants::MAX_RCVD_IMAGE_PIXELS => {
part.param.set_i64(Param::Width, width.into());
part.param.set_i64(Param::Height, height.into());
} else if msg_type == Viewtype::Vcard {
if let Some(summary) = get_vcard_summary(decoded_data) {
part.param.set(Param::Summary1, summary);
msg_type
} else {
Viewtype::File
}
// image is too big or size is unknown, display as file:
_ => Viewtype::File,
}
} else {
msg_type
};
} else if msg_type == Viewtype::Image
|| msg_type == Viewtype::Gif
|| msg_type == Viewtype::Sticker
{
match get_filemeta(decoded_data) {
// image size is known, not too big, keep msg_type:
Ok((width, height)) if width * height <= constants::MAX_RCVD_IMAGE_PIXELS => {
part.param.set_i64(Param::Width, width.into());
part.param.set_i64(Param::Height, height.into());
msg_type
}
// image is too big or size is unknown, display as file:
_ => Viewtype::File,
}
} else {
msg_type
};
/* we have a regular file attachment,
write decoded data to new blob object */
-22
View File
@@ -244,28 +244,6 @@ fn find_zip_entry<'a>(
const STATUS_UPDATE_SIZE_MAX: usize = 100 << 10;
impl Context {
/// check if a file is an acceptable webxdc for sending or receiving.
pub(crate) async fn is_webxdc_file(&self, filename: &str, file: &[u8]) -> Result<bool> {
if !filename.ends_with(WEBXDC_SUFFIX) {
return Ok(false);
}
let archive = match async_zip::base::read::mem::ZipFileReader::new(file.to_vec()).await {
Ok(archive) => archive,
Err(_) => {
info!(self, "{} cannot be opened as zip-file", &filename);
return Ok(false);
}
};
if find_zip_entry(archive.file(), "index.html").is_none() {
info!(self, "{} misses index.html", &filename);
return Ok(false);
}
Ok(true)
}
/// Ensure that a file is an acceptable webxdc for sending.
pub(crate) async fn ensure_sendable_webxdc_file(&self, path: &Path) -> Result<()> {
let filename = path.to_str().unwrap_or_default();
+2 -43
View File
@@ -17,47 +17,6 @@ use crate::test_utils::{E2EE_INFO_MSGS, TestContext, TestContextManager};
use crate::tools::{self, SystemTime};
use crate::{message, sql};
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_is_webxdc_file() -> Result<()> {
let t = TestContext::new().await;
assert!(
!t.is_webxdc_file(
"bad-ext-no-zip.txt",
include_bytes!("../../test-data/message/issue_523.txt")
)
.await?
);
assert!(
!t.is_webxdc_file(
"bad-ext-good-zip.txt",
include_bytes!("../../test-data/webxdc/minimal.xdc")
)
.await?
);
assert!(
!t.is_webxdc_file(
"good-ext-no-zip.xdc",
include_bytes!("../../test-data/message/issue_523.txt")
)
.await?
);
assert!(
!t.is_webxdc_file(
"good-ext-no-index-html.xdc",
include_bytes!("../../test-data/webxdc/no-index-html.xdc")
)
.await?
);
assert!(
t.is_webxdc_file(
"good-ext-good-zip.xdc",
include_bytes!("../../test-data/webxdc/minimal.xdc")
)
.await?
);
Ok(())
}
fn create_webxdc_instance(t: &TestContext, name: &str, bytes: &[u8]) -> Result<Message> {
let mut instance = Message::new(Viewtype::File);
instance.set_file_from_bytes(t, name, bytes, None)?;
@@ -264,11 +223,11 @@ async fn test_receive_webxdc_instance() -> Result<()> {
test_utils::receive_encrypted_imf(
alice,
bob,
include_bytes!("../../test-data/message/webxdc_good_extension.eml"),
include_bytes!("../../test-data/message/webxdc_bad_mime_type.eml"),
)
.await?;
let instance = alice.get_last_msg().await;
assert_eq!(instance.viewtype, Viewtype::Webxdc);
assert_eq!(instance.viewtype, Viewtype::File);
assert_eq!(instance.get_filename().unwrap(), "minimal.xdc");
test_utils::receive_encrypted_imf(
@@ -10,9 +10,8 @@ Content-Type: multipart/mixed; boundary="==BREAK=="
--==BREAK==
Content-Type: text/plain; charset=utf-8
webxdc with good extension;
the mimetype is ignored then,
content is checked.
webxdc with bad mime type,
not detected as a webxdc
--==BREAK==
Content-Type: text/html
Binary file not shown.