Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 22 additions & 42 deletions crates/symbolicator-native/src/symbolication/attachments.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
use std::fs::File;
use std::sync::Arc;

use futures::TryStreamExt;
use symbolicator_service::caching::CacheError;
use symbolicator_service::download::{self, DownloadService};
use symbolicator_service::utils::fs;
use tokio::io::{AsyncSeekExt, AsyncWriteExt, BufWriter};
use symbolicator_service::download::{DownloadService, fetch_file};
use symbolicator_sources::{HttpRemoteFile, RemoteFile};
use url::Url;

use crate::interface::AttachmentFile;

#[tracing::instrument(skip(download_svc))]
pub async fn download_attachment(
download_svc: &DownloadService,
download_svc: Arc<DownloadService>,
file: AttachmentFile,
) -> Result<File, CacheError> {
) -> anyhow::Result<File> {
let (storage_url, storage_token) = match file {
AttachmentFile::Local(file) => return Ok(file),
AttachmentFile::Remote {
Expand All @@ -21,39 +20,20 @@
} => (storage_url, storage_token),
};

// TODO: maybe its worth using the actual `DownloadService` instead of straight going to the `trusted_client`.
// Doing so would in theory allow us to have retries and error report, as well as being able to
// download files in multiple chunks concurrently, but I don’t think our `objecstore` server currently
// supports range requests, and those would also mess with streaming decompression.
// Not to mention that using the `DownloadService` is not that straight forward.
download::retry(|| async {
let mut request = download_svc.trusted_client.get(&storage_url);
if let Some(token) = storage_token.as_ref() {
request = request.bearer_auth(token);
}
let response = request.send().await?;
if !response.status().is_success() {
return Err(download::GenericErrorHandler::handle_status(
&storage_url,
response.status(),
)
.await);
}

let mut stream = response.bytes_stream();

let file = fs::tempfile(download_svc.tmp_dir.as_deref())?.into_file();
let mut writer = BufWriter::new(tokio::fs::File::from_std(file));
while let Some(chunk) = stream.try_next().await? {
writer.write_all(&chunk).await?;
}
writer.flush().await?;
let mut file = writer.into_inner();
file.sync_data().await?;

file.rewind().await?;

Ok(file.into_std().await)
})
.await
let mut http_remote_file = HttpRemoteFile::from_url(Url::parse(&storage_url)?, true);

if let Some(token) = storage_token {
http_remote_file = http_remote_file.bearer_auth(&token);
}

let mut temp_file = tempfile::NamedTempFile::new()?;

fetch_file(
download_svc,
RemoteFile::Http(http_remote_file),
&mut temp_file,
)
.await?;

Check warning on line 36 in crates/symbolicator-native/src/symbolication/attachments.rs

View check run for this annotation

@sentry/warden / warden: wrdn-dos-review

Remote minidump attachment decompression has no effective CPU bound despite 15GiB output cap

An attacker-controlled storage URL can serve a tiny zstd/gzip/zlib/zip/CAB payload that `fetch_file` synchronously inflates into a temporary file after the download timeout has ended. Although output is capped at the default 15GiB, the decompression and subsequent minidump processing have no decompression-specific CPU budget; the outer request timeout cannot preempt this synchronous block, allowing substantial CPU and disk exhaustion on the shared worker pool.
Comment on lines +31 to +36

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remote minidump attachment decompression has no effective CPU bound despite 15GiB output cap

An attacker-controlled storage URL can serve a tiny zstd/gzip/zlib/zip/CAB payload that fetch_file synchronously inflates into a temporary file after the download timeout has ended. Although output is capped at the default 15GiB, the decompression and subsequent minidump processing have no decompression-specific CPU budget; the outer request timeout cannot preempt this synchronous block, allowing substantial CPU and disk exhaustion on the shared worker pool.

Evidence
  • /symbolicate-any accepts attacker-controlled storage_url values and constructs AttachmentFile::Remote; process_minidump passes them to download_attachment (crates/symbolicator/src/endpoints/symbolicate_any.rs:42-72, crates/symbolicator-native/src/symbolication/process_minidump.rs:602-634).
  • download_attachment calls fetch_file (crates/symbolicator-native/src/symbolication/attachments.rs:23-36), which invokes DownloadService::download and then synchronously calls maybe_decompress_file after the download timeout (crates/symbolicator-service/src/download/fetch_file.rs:18-37).
  • maybe_decompress_file uses take(max_uncompressed_size + 1) but still copies up to the configured limit into a temporary file before checking its size; the shipped default is 15GiB and no CPU or decompression-time budget exists (crates/symbolicator-service/src/download/compression.rs:31-145, crates/symbolicator-service/src/config.rs:553-556,670-674).
  • The resulting file is mapped and parsed before minidump stackwalking (crates/symbolicator-native/src/symbolication/process_minidump.rs:628-634,491); processing runs on the CPU pool and the default request admission limit allows 200 concurrent requests (crates/symbolicator/src/service.rs:242-259,428-518).

Identified by Warden · wrdn-dos-review · MZN-DLX


Ok(temp_file.into_file())
}
Original file line number Diff line number Diff line change
Expand Up @@ -625,7 +625,7 @@ impl SymbolicationActor {
rewrite_first_module,
extract_variables,
} = request;
let minidump_file = download_attachment(&self.download_svc, minidump_file).await?;
let minidump_file = download_attachment(self.download_svc.clone(), minidump_file).await?;
let len = minidump_file.metadata()?.len();
tracing::debug!("Processing minidump ({} bytes)", len);
metric!(distribution("minidump.upload.size") = len as f64);
Expand Down
8 changes: 8 additions & 0 deletions crates/symbolicator-sources/src/sources/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,14 @@ impl HttpRemoteFile {
HttpRemoteFile::new(source, location)
}

/// Adds bearer authorization to the request and returns the updated file.
pub fn bearer_auth(mut self, token: &str) -> Self {
self.headers
.0
.insert("Authorization".to_owned(), format!("Bearer {token}"));
self
}

/// Returns a [`RemoteFileUri`] for the file.
pub fn uri(&self) -> RemoteFileUri {
match self.url() {
Expand Down
Loading