From b656a6023421330ece465e80e5964e011f3a337e Mon Sep 17 00:00:00 2001 From: "B. Petersen" Date: Tue, 28 Dec 2021 17:54:06 +0100 Subject: [PATCH] check that the w30 app is actually an zip-archive with an index.html --- Cargo.lock | 13 +++ Cargo.toml | 1 + deltachat-ffi/deltachat.h | 14 +++ deltachat-ffi/src/lib.rs | 33 ++++++ src/message.rs | 4 +- src/w30.rs | 135 ++++++++++++++++++++--- test-data/message/w30_good_extension.eml | 13 ++- test-data/w30/minimal.w30 | Bin 293 -> 294 bytes test-data/w30/no-index-html.w30 | Bin 0 -> 239 bytes test-data/w30/some-files.w30 | Bin 0 -> 2105 bytes 10 files changed, 195 insertions(+), 18 deletions(-) create mode 100644 test-data/w30/no-index-html.w30 create mode 100644 test-data/w30/some-files.w30 diff --git a/Cargo.lock b/Cargo.lock index 2f0e3758d..c871df8b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1131,6 +1131,7 @@ dependencies = [ "toml", "url", "uuid", + "zip", ] [[package]] @@ -4254,3 +4255,15 @@ dependencies = [ "syn", "synstructure", ] + +[[package]] +name = "zip" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93ab48844d61251bb3835145c521d88aa4031d7139e8485990f60ca911fa0815" +dependencies = [ + "byteorder", + "crc32fast", + "flate2", + "thiserror", +] diff --git a/Cargo.toml b/Cargo.toml index e52273547..ff998fd4e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -76,6 +76,7 @@ humansize = "1" qrcodegen = "1.7.0" tagger = "4.0.1" textwrap = "0.14.2" +zip = { version = "0.5.13", default-features = false, features = ["deflate"] } [dev-dependencies] ansi_term = "0.12.0" diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index 6b3e9dd4a..7b89adf2c 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -3675,6 +3675,20 @@ char* dc_msg_get_filename (const dc_msg_t* msg); char* dc_msg_get_filemime (const dc_msg_t* msg); +/** + * Return file from inside an archive. + * Currently, this works for W30 messages only. + * + * @param msg The W30 instance. + * @param filename The name inside the archive, + * must be given as a relative path (no leading `/`). + * @param ret_bytes Pointer to a size_t. The size of the blob will be written here. + * @return The blob must be released using dc_str_unref() after usage. + * NULL if there is no such file in the archive or on errors. + */ +char* dc_msg_get_blob_from_archive (const dc_msg_t* msg, const char* filename, size_t* ret_bytes); + + /** * Get the size of the file. Returns the size of the file associated with a * message, if applicable. diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index c87dc326f..df15d9c00 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -3070,6 +3070,39 @@ pub unsafe extern "C" fn dc_msg_get_filename(msg: *mut dc_msg_t) -> *mut libc::c ffi_msg.message.get_filename().unwrap_or_default().strdup() } +#[no_mangle] +pub unsafe extern "C" fn dc_msg_get_blob_from_archive( + msg: *mut dc_msg_t, + filename: *const libc::c_char, + ret_bytes: *mut libc::size_t, +) -> *mut libc::c_char { + if msg.is_null() || filename.is_null() || ret_bytes.is_null() { + eprintln!("ignoring careless call to dc_msg_get_blob_from_archive()"); + return ptr::null_mut(); + } + let ffi_msg = &*msg; + let ctx = &*ffi_msg.context; + let blob = block_on(async move { + ffi_msg + .message + .get_blob_from_archive(ctx, &to_string_lossy(filename)) + .await + }); + match blob { + Ok(blob) => { + // TODO: introduce dc_blob_t to avoid malloc and returning size by pointer and to save copying data + *ret_bytes = blob.len(); + let ptr = libc::malloc(*ret_bytes); + libc::memcpy(ptr, blob.as_ptr() as *mut libc::c_void, *ret_bytes); + ptr as *mut libc::c_char + } + Err(err) => { + eprintln!("failed read blob from archive: {}", err); + ptr::null_mut() + } + } +} + #[no_mangle] pub unsafe extern "C" fn dc_msg_get_filemime(msg: *mut dc_msg_t) -> *mut libc::c_char { if msg.is_null() { diff --git a/src/message.rs b/src/message.rs index 06e02b673..6067c5de6 100644 --- a/src/message.rs +++ b/src/message.rs @@ -1168,7 +1168,7 @@ pub fn guess_msgtype_from_suffix(path: &Path) -> Option<(Viewtype, &str)> { "ttf" => (Viewtype::File, "font/ttf"), "vcard" => (Viewtype::File, "text/vcard"), "vcf" => (Viewtype::File, "text/vcard"), - "w30" => (Viewtype::W30, "application/html+w30"), + "w30" => (Viewtype::W30, "application/w30+zip"), "wav" => (Viewtype::File, "audio/wav"), "weba" => (Viewtype::File, "audio/webm"), "webm" => (Viewtype::Video, "video/webm"), @@ -1705,7 +1705,7 @@ mod tests { ); assert_eq!( guess_msgtype_from_suffix(Path::new("foo/file.w30")), - Some((Viewtype::W30, "application/html+w30")) + Some((Viewtype::W30, "application/w30+zip")) ); } diff --git a/src/w30.rs b/src/w30.rs index b22e9b7ff..cf1101788 100644 --- a/src/w30.rs +++ b/src/w30.rs @@ -2,16 +2,18 @@ use crate::constants::Viewtype; use crate::context::Context; +use crate::dc_tools::dc_open_file_std; use crate::message::{Message, MessageState, MsgId}; use crate::mimeparser::SystemMessage; use crate::param::Param; use crate::{chat, EventType}; -use anyhow::{bail, Result}; +use anyhow::{bail, ensure, format_err, Result}; use lettre_email::mime::{self}; use lettre_email::PartBuilder; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::convert::TryFrom; +use std::io::Read; pub const W30_SUFFIX: &str = "w30"; @@ -49,12 +51,16 @@ pub(crate) struct StatusUpdateItem { } impl Context { - pub(crate) async fn is_w30_file(&self, filename: &str, _decoded_data: &[u8]) -> Result { + pub(crate) async fn is_w30_file(&self, filename: &str, buf: &[u8]) -> Result { if filename.ends_with(W30_SUFFIX) { - Ok(true) - } else { - Ok(false) + let reader = std::io::Cursor::new(buf); + if let Ok(mut archive) = zip::ZipArchive::new(reader) { + if let Ok(_index_html) = archive.by_name("index.html") { + return Ok(true); + } + } } + Ok(false) } async fn create_status_update_record( @@ -221,6 +227,26 @@ impl Context { } } +impl Message { + /// Return file form inside an archive. + /// Currently, this works only if the message is an w30 instance. + pub async fn get_blob_from_archive(&self, context: &Context, name: &str) -> Result> { + ensure!(self.viewtype == Viewtype::W30, "No w30 instance."); + + let archive = self + .get_file(context) + .ok_or_else(|| format_err!("No w30 instance file."))?; + let archive = dc_open_file_std(context, archive)?; + let mut archive = zip::ZipArchive::new(archive)?; + + let mut file = archive.by_name(name)?; + + let mut buf = Vec::new(); + file.read_to_end(&mut buf)?; + Ok(buf) + } +} + #[cfg(test)] mod tests { use super::*; @@ -239,14 +265,35 @@ mod tests { let t = TestContext::new().await; assert!( !t.is_w30_file( - "issue_523.txt", + "bad-ext-no-zip.txt", include_bytes!("../test-data/message/issue_523.txt") ) .await? ); + assert!( + !t.is_w30_file( + "bad-ext-good-zip.txt", + include_bytes!("../test-data/w30/minimal.w30") + ) + .await? + ); + assert!( + !t.is_w30_file( + "good-ext-no-zip.w30", + include_bytes!("../test-data/message/issue_523.txt") + ) + .await? + ); + assert!( + !t.is_w30_file( + "good-ext-no-index-html.w30", + include_bytes!("../test-data/w30/no-index-html.w30") + ) + .await? + ); assert!( t.is_w30_file( - "minimal.w30", + "good-ext-good-zip.w30", include_bytes!("../test-data/w30/minimal.w30") ) .await? @@ -255,10 +302,10 @@ mod tests { } async fn create_w30_instance(t: &TestContext) -> Result { - let file = t.get_blobdir().join("index.w30"); + let file = t.get_blobdir().join("minimal.w30"); File::create(&file) .await? - .write_all("ola!".as_ref()) + .write_all(include_bytes!("../test-data/w30/minimal.w30")) .await?; let mut instance = Message::new(Viewtype::File); instance.set_file(file.to_str().unwrap(), None); @@ -279,7 +326,7 @@ mod tests { // send as .w30 file let instance = send_w30_instance(&t, chat_id).await?; assert_eq!(instance.viewtype, Viewtype::W30); - assert_eq!(instance.get_filename(), Some("index.w30".to_string())); + assert_eq!(instance.get_filename(), Some("minimal.w30".to_string())); assert_eq!(instance.chat_id, chat_id); // sending using bad extension is not working, even when setting Viewtype to W30 @@ -308,7 +355,7 @@ mod tests { .await?; let instance = t.get_last_msg().await; assert_eq!(instance.viewtype, Viewtype::W30); - assert_eq!(instance.get_filename(), Some("index.w30".to_string())); + assert_eq!(instance.get_filename(), Some("minimal.w30".to_string())); dc_receive_imf( &t, @@ -596,14 +643,17 @@ mod tests { let sent1 = alice.pop_sent_msg().await; let alice_instance = Message::load_from_db(&alice, alice_instance_id).await?; assert_eq!(alice_instance.viewtype, Viewtype::W30); - assert_eq!(alice_instance.get_filename(), Some("index.w30".to_string())); + assert_eq!( + alice_instance.get_filename(), + Some("minimal.w30".to_string()) + ); assert_eq!(alice_instance.chat_id, alice_chat_id); // bob receives the instance together with the initial updates in a single message bob.recv_msg(&sent1).await; let bob_instance = bob.get_last_msg().await; assert_eq!(bob_instance.viewtype, Viewtype::W30); - assert_eq!(bob_instance.get_filename(), Some("index.w30".to_string())); + assert_eq!(bob_instance.get_filename(), Some("minimal.w30".to_string())); assert!(sent1.payload().contains("Content-Type: application/json")); assert!(sent1.payload().contains("status-update.json")); assert!(sent1.payload().contains(r#""payload":{"foo":"bar"}"#)); @@ -627,4 +677,63 @@ mod tests { .is_err()); Ok(()) } + + #[async_std::test] + async fn test_get_blob_from_archive() -> Result<()> { + let t = TestContext::new_alice().await; + let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; + let instance = send_w30_instance(&t, chat_id).await?; + + let buf = instance.get_blob_from_archive(&t, "index.html").await?; + assert_eq!(buf.len(), 188); + assert!(String::from_utf8_lossy(&buf).contains("document.write")); + + assert!(instance + .get_blob_from_archive(&t, "not-existent.html") + .await + .is_err()); + Ok(()) + } + + #[async_std::test] + async fn test_get_blob_from_archive_subdirs() -> Result<()> { + let t = TestContext::new_alice().await; + let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; + let file = t.get_blobdir().join("some-files.w30"); + File::create(&file) + .await? + .write_all(include_bytes!("../test-data/w30/some-files.w30")) + .await?; + let mut instance = Message::new(Viewtype::W30); + instance.set_file(file.to_str().unwrap(), None); + chat_id.set_draft(&t, Some(&mut instance)).await?; + + let buf = instance.get_blob_from_archive(&t, "index.html").await?; + assert_eq!(buf.len(), 65); + assert!(String::from_utf8_lossy(&buf).contains("many files")); + + let buf = instance.get_blob_from_archive(&t, "subdir/bla.txt").await?; + assert_eq!(buf.len(), 4); + assert!(String::from_utf8_lossy(&buf).starts_with("bla")); + + let buf = instance + .get_blob_from_archive(&t, "subdir/subsubdir/text.md") + .await?; + assert_eq!(buf.len(), 24); + assert!(String::from_utf8_lossy(&buf).starts_with("this is a markdown file")); + + let buf = instance + .get_blob_from_archive(&t, "subdir/subsubdir/text2.md") + .await?; + assert_eq!(buf.len(), 22); + assert!(String::from_utf8_lossy(&buf).starts_with("another markdown")); + + let buf = instance + .get_blob_from_archive(&t, "anotherdir/anothersubsubdir/foo.txt") + .await?; + assert_eq!(buf.len(), 4); + assert!(String::from_utf8_lossy(&buf).starts_with("foo")); + + Ok(()) + } } diff --git a/test-data/message/w30_good_extension.eml b/test-data/message/w30_good_extension.eml index 756bca416..48b68cb0c 100644 --- a/test-data/message/w30_good_extension.eml +++ b/test-data/message/w30_good_extension.eml @@ -11,12 +11,19 @@ Content-Type: multipart/mixed; boundary="==BREAK==" Content-Type: text/plain; charset=utf-8 w30 with good extension; -the mimetype is ignored then. +the mimetype is ignored then, +content is checked. --==BREAK== Content-Type: text/html -Content-Disposition: attachment; filename=index.w30 +Content-Disposition: attachment; filename=minimal.w30 +Content-Transfer-Encoding: base64 -hey! +UEsDBBQAAgAIAFJqnVOItjSofAAAALwAAAAKABwAaW5kZXguaHRtbFVUCQADG1LMYV1SzGF1eAsAAQ +T1AQAABBQAAACzUXTxdw6JDHBVyCjJzbHjsoFQCgo2SfkplSCGgkJJanGJIphlU5xclFlQAhFWUEjJ +Ty7NTc0r0SsvyixJ1VAqyU8EqlTStIYo1kdWbZOXj6o5uiQjsxhodkWJQnFGfmlOikJefolCUiqIV5 +4XCzUCWZeNPsRNNvoQRwIAUEsBAh4DFAACAAgAUmqdU4i2NKh8AAAAvAAAAAoAGAAAAAAAAQAAAKSB +AAAAAGluZGV4Lmh0bWxVVAUAAxtSzGF1eAsAAQT1AQAABBQAAABQSwUGAAAAAAEAAQBQAAAAwAAAAA +AA --==BREAK==-- diff --git a/test-data/w30/minimal.w30 b/test-data/w30/minimal.w30 index 63149c354201484bbec50a4db966f3443e2e4910..a53bd101d15a60e120f48ffdb4ced78cf0c7a817 100644 GIT binary patch delta 113 zcmZ3=w2a9lz?+#xgn@~HgCQtuZg9solNB`#3=DgKn2SM%Au}%}wL&kWBsV8Cgp+|; zI_OMdY|xpBCTc96KQ*ULjEj*r`z7Gc#1P=k$Rx*%X3oSb8Xz4L|0}Q^0Gh|Z006Sh BA87yp delta 112 zcmZ3+w3Nvtz?+#xgn@~HgCVG3POz%yt>x7W3=DgKn2SM%Au}%}wL&kWBsV8Cgp+~! z4cFi>!u@f&WN-Y zB{R34*)!z|SL>EDb3$5O7BmUXn6>A~ohwPLEE~4So#=l{V}-a3%~b*3tZX2Kj6moQq`N>I1^_xpLxTVS literal 0 HcmV?d00001 diff --git a/test-data/w30/some-files.w30 b/test-data/w30/some-files.w30 new file mode 100644 index 0000000000000000000000000000000000000000..0e37febdf0e76fc7a01f9f3b0851b98162a5ab72 GIT binary patch literal 2105 zcmbVNO-vI(7@ckrY7r<2kSd^{1WGJzuH>TQP%GWl#l+fsp7J>`@%9#d_=T zg0A5@8jIRH(M-$4>W7&hPyhXHnpl*dwL28g+4;Hk6h84B*lD}UF#G;Y?2UvT>^g~; zj%}PAcgEZem!cl~a%;?)@VetN`z8l>_|-HM|SoF=P7vp`Jj<(I*ZL zDC3S{E_hAQHRCR{iND4QAMd-}5w*oWzWVZ7;?3sc>_+`m)uRW^L;f3c%~yL{Zyj@A zJ9Jo>Eo$4V7~xy|x!Q=ifKOk^Jib$kmH2!f*V&8SD^7Y~DlHHHmRrNjWOcQRuXQgmRb^+q@57eq^PdtcoZS{>SK_SQ zQQy$&VOcxdQE1AoA72WdDsDO56g+pZ#i-~HGtrvsSRtt7IRjH-%p5HE5v7ta?9O6F z1*YQwU?6BnC0#7Or1EPQv6Etg?Ep-4HbC%@ib%8DFl8A-v}byeLkN?u0|u0R0I7b_ zX9~a|f=DG@J{43w-^xyh6O}QLRFfc*=qQ0mA{CKds_^BGNR*-Bob# T+#dKk;0!b)8G~Gg?+yA7i+%dY literal 0 HcmV?d00001