mirror of
https://github.com/chatmail/core.git
synced 2026-10-18 02:43:02 +03:00
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:
+57
-60
@@ -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 */
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(
|
||||
|
||||
+2
-3
@@ -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.
Reference in New Issue
Block a user