Denial of service, medium severity.
src/runtime/image/Image.rs:1300
Bun.Image enforces MAX_INPUT_FILE_BYTES for path-backed image inputs before reading file contents, but Blob-backed inputs from Bun.file() bypass that guard. A file-backed Blob larger than the 256 MiB encoded input cap can be fully read into memory before image validation runs, allowing attacker-influenced image paths to trigger process memory exhaustion.
Verified finding from Swival.dev Security Scanner: https://swival.dev
Confidence: certain.
- The application constructs
Bun.Imagefrom an attacker-influencedBun.file()Blob. - A terminal image operation is invoked, such as
.bytes(),.buffer(),.blob(),.metadata(),.toBase64(),.dataURL(),.placeholder(), or.write().
Reachable path:
source_from_jsstores non-memory Blob inputs asSource::Blobwhenblob.storeexists.scheduledivertsSource::BlobintoBlobReadChain::start.BlobReadChain::startcallsblob.read_bytes_to_handler.BlobReadChain::on_read_bytes_implacceptedReadBytesResult::Ok(bytes)and swapped it intoSource::Owned(bytes)without checkingMAX_INPUT_FILE_BYTES.- The existing 256 MiB encoded input cap only ran for
Source::PathinPipelineTask::runbeforefile.read_to_end. - File-backed Blob reads use
ReadFileand can preallocate/read the whole file fromst_size, so oversized files can be materialized before image guards run.
Representative trigger:
await new Bun.Image(Bun.file(attackerControlledPath)).bytes();The same bypass applies to .metadata() and other terminal operations because all Source::Blob terminals pass through BlobReadChain.
The code has an explicit encoded-file-size guard, MAX_INPUT_FILE_BYTES, intended to prevent large encoded inputs from being materialized before header or pixel guards run. That guard is enforced for path sources but not for file-backed Blob sources representing the same underlying file class. Since Blob reading completes before the image pipeline sees the bytes, an oversized file-backed Blob can consume large process memory before maxPixels, decode checks, or the path-specific size cap can reject it.
Enforce MAX_INPUT_FILE_BYTES on Blob read completion before storing Blob bytes in Source::Owned or scheduling the image pipeline.
The patch adds a size check immediately after ReadBytesResult::Ok(bytes) is received in BlobReadChain::on_read_bytes_impl:
if bytes.len() as u64 > MAX_INPUT_FILE_BYTES {
drop(deliver);
let _ = outer.reject(
global,
Ok(reject_error(global, codecs::Error::TooManyPixels)),
);
return;
}This is the earliest point in the Blob read chain where the completed byte vector size is available. Rejecting there prevents the oversized buffer from being cached as Source::Owned and prevents re-entry into the image pipeline. The rejection reuses the same TooManyPixels error path used by the existing encoded-file cap for path-backed inputs.
None
diff --git a/src/runtime/image/Image.rs b/src/runtime/image/Image.rs
index 7d129f0ebc..2e778170af 100644
--- a/src/runtime/image/Image.rs
+++ b/src/runtime/image/Image.rs
@@ -1316,6 +1316,14 @@ impl<'a> BlobReadChain<'a> {
match r {
ReadBytesResult::Ok(bytes) => {
+ if bytes.len() as u64 > MAX_INPUT_FILE_BYTES {
+ drop(deliver);
+ let _ = outer.reject(
+ global,
+ Ok(reject_error(global, codecs::Error::TooManyPixels)),
+ );
+ return;
+ }
// Concurrent terminals can have started multiple BlobReadChains
// (no in-flight serialisation — `start()` re-enters every time
// it sees `.blob`). The FIRST resolver wins and swaps to