diff --git a/relay-server/src/endpoints/minidump.rs b/relay-server/src/endpoints/minidump.rs index 80d513d709e..80a8e223e22 100644 --- a/relay-server/src/endpoints/minidump.rs +++ b/relay-server/src/endpoints/minidump.rs @@ -1,10 +1,13 @@ use std::convert::Infallible; +use std::io::Cursor; +use std::io::Read; use axum::extract::{DefaultBodyLimit, Request}; use axum::response::IntoResponse; use axum::routing::{post, MethodRouter}; use axum::RequestExt; use bytes::Bytes; +use flate2::read::GzDecoder; use multer::Multipart; use relay_config::Config; use relay_event_schema::protocol::EventId; @@ -31,6 +34,8 @@ const MINIDUMP_FILE_NAME: &str = "Minidump"; const MINIDUMP_MAGIC_HEADER_LE: &[u8] = b"MDMP"; const MINIDUMP_MAGIC_HEADER_BE: &[u8] = b"PMDM"; +const GZIP_MAGIC_HEADER: &[u8] = &[0x1f, 0x8b]; + /// Content types by which standalone uploads can be recognized. const MINIDUMP_RAW_CONTENT_TYPES: &[&str] = &["application/octet-stream", "application/x-dmp"]; @@ -43,6 +48,24 @@ fn validate_minidump(data: &[u8]) -> Result<(), BadStoreRequest> { Ok(()) } +fn validate_gzip_minidump(data: &[u8]) -> Result<(), BadStoreRequest> { + if !data.starts_with(GZIP_MAGIC_HEADER) { + relay_log::trace!("invalid minidump file"); + return Err(BadStoreRequest::InvalidMinidump); + } + + let mut decoder = GzDecoder::new(data); + let mut magic_bytes = [0u8; 4]; + + match decoder.read_exact(&mut magic_bytes) { + Ok(_) => validate_minidump(&magic_bytes), + Err(_) => { + relay_log::trace!("invalid minidump file"); + Err(BadStoreRequest::InvalidMinidump) + } + } +} + fn infer_attachment_type(field_name: Option<&str>) -> AttachmentType { match field_name.unwrap_or("") { MINIDUMP_FIELD_NAME => AttachmentType::Minidump, @@ -97,7 +120,11 @@ async fn extract_multipart( minidump_item.set_payload(ContentType::Minidump, embedded); } - validate_minidump(&minidump_item.payload())?; + if validate_minidump(&minidump_item.payload()).is_err() { + validate_gzip_minidump(&minidump_item.payload())?; + let unzipped = extract_minidump_from_gzip(minidump_item.payload())?; + minidump_item.set_payload(ContentType::Minidump, unzipped); + } let event_id = common::event_id_from_items(&items)?.unwrap_or_else(EventId::new); let mut envelope = Envelope::from_request(Some(event_id), meta); @@ -109,11 +136,31 @@ async fn extract_multipart( Ok(envelope) } -fn extract_raw_minidump(data: Bytes, meta: RequestMeta) -> Result, BadStoreRequest> { - validate_minidump(&data)?; +fn extract_minidump_from_gzip(data: Bytes) -> Result { + let cursor = Cursor::new(data); + let mut decoder = GzDecoder::new(cursor); + let mut buffer = Vec::new(); + match decoder.read_to_end(&mut buffer) { + Ok(_) => Ok(Bytes::from(buffer)), + Err(_) => { + relay_log::trace!("invalid minidump file"); + Err(BadStoreRequest::InvalidMinidump) + } + } +} +fn extract_raw_minidump(data: Bytes, meta: RequestMeta) -> Result, BadStoreRequest> { let mut item = Item::new(ItemType::Attachment); - item.set_payload(ContentType::Minidump, data); + + match validate_minidump(&data) { + Ok(_) => item.set_payload(ContentType::Minidump, data), + Err(_) => { + validate_gzip_minidump(&data)?; + let unzipped = extract_minidump_from_gzip(data)?; + item.set_payload(ContentType::Minidump, unzipped); + } + } + item.set_filename(MINIDUMP_FILE_NAME); item.set_attachment_type(AttachmentType::Minidump); @@ -163,7 +210,10 @@ pub fn route(config: &Config) -> MethodRouter { #[cfg(test)] mod tests { use axum::body::Body; + use flate2::write::GzEncoder; + use flate2::Compression; use relay_config::Config; + use std::io::Write; use crate::utils::{multipart_items, FormDataIter}; @@ -181,6 +231,30 @@ mod tests { assert!(validate_minidump(garbage).is_err()); } + fn encode_gzip(be_minidump: &[u8]) -> Result, Box> { + let mut encoder = GzEncoder::new(Vec::new(), Compression::default()); + encoder.write_all(be_minidump)?; + let compressed = encoder.finish()?; + Ok(compressed) + } + + #[test] + fn test_validate_gzip_minidump() -> Result<(), Box> { + let be_minidump = b"PMDMxxxxxx"; + let compressed = encode_gzip(be_minidump).unwrap(); + assert!(validate_gzip_minidump(&compressed).is_ok()); + + let le_minidump = b"MDMPxxxxxx"; + let compressed = encode_gzip(le_minidump).unwrap(); + assert!(validate_gzip_minidump(&compressed).is_ok()); + + let garbage = b"xxxxxx"; + let compressed = encode_gzip(garbage).unwrap(); + assert!(validate_gzip_minidump(&compressed).is_err()); + + Ok(()) + } + #[tokio::test] async fn test_minidump_multipart_attachments() -> anyhow::Result<()> { let multipart_body: &[u8] = diff --git a/tests/integration/test_minidump.py b/tests/integration/test_minidump.py index ad1368ac97c..3518d9cfc22 100644 --- a/tests/integration/test_minidump.py +++ b/tests/integration/test_minidump.py @@ -359,11 +359,25 @@ def test_minidump_invalid_nested_formdata(mini_sentry, relay): relay.send_minidump(project_id=project_id, files=attachments) -@pytest.mark.parametrize("rate_limit", [None, "attachment", "transaction"]) +@pytest.mark.parametrize( + "rate_limit,minidump_filename", + [ + (None, "minidump.dmp"), + ("attachment", "minidump.dmp"), + ("transaction", "minidump.dmp"), + (None, "minidump.dmp.gz"), + ], +) def test_minidump_with_processing( - mini_sentry, relay_with_processing, attachments_consumer, rate_limit + mini_sentry, + relay_with_processing, + attachments_consumer, + rate_limit, + minidump_filename, ): - dmp_path = os.path.join(os.path.dirname(__file__), "fixtures/native/minidump.dmp") + dmp_path = os.path.join( + os.path.dirname(__file__), f"fixtures/native/{minidump_filename}" + ) with open(dmp_path, "rb") as f: content = f.read() @@ -392,7 +406,7 @@ def test_minidump_with_processing( attachments_consumer = attachments_consumer() - attachments = [(MINIDUMP_ATTACHMENT_NAME, "minidump.dmp", content)] + attachments = [(MINIDUMP_ATTACHMENT_NAME, minidump_filename, content)] response = relay.send_minidump(project_id=project_id, files=attachments) attachment = b"" @@ -419,7 +433,7 @@ def test_minidump_with_processing( assert list(message["attachments"]) == [ { "id": attachment_id, - "name": "minidump.dmp", + "name": minidump_filename, "attachment_type": "event.minidump", "chunks": num_chunks, "size": len(content),