Skip to content

fix(usvg): limit SVGZ decompression size to prevent decompression bombs - #1139

Open
akashchamp wants to merge 1 commit into
linebender:mainfrom
akashchamp:fix/svgz-decompression-bomb
Open

akashchamp wants to merge 1 commit into
linebender:mainfrom
akashchamp:fix/svgz-decompression-bomb

Conversation

@akashchamp

@akashchamp akashchamp commented Sep 24, 2026 •

Copy link
Copy Markdown

Problem

usvg::Tree::from_data decompresses .svgz (GZip-compressed SVG) input via
decompress_svgz, which called GzDecoder::read_to_end with no bound on the
decompressed size:

let mut decoder = flate2::read::GzDecoder::new(data);
let mut decoded = Vec::with_capacity(data.len() * 2);
decoder.read_to_end(&mut decoded).map_err(|_| Error::MalformedGZip)?;

GZip supports very high compression ratios, so a small, maliciously crafted
.svgz file can decompress into an enormous amount of data, exhausting
available memory (a "decompression bomb" / zip-bomb style DoS). Any
application that calls Tree::from_data on untrusted input is affected.

Publicly disclosed in #1138 (write-up:
https://fereidani.com/usvg-svgz-decompression-bomb-in-treefromdata).

I confirmed it locally: a ~498 KiB crafted .svgz file (highly compressible,
repeated zero bytes) decompressed to 500 MiB in ~0.3s / ~580 MiB RSS with the
unpatched code. A larger crafted input reaches gigabytes just as easily.

Fix

Cap the number of bytes decompress_svgz will read out of the decoder at
1 GiB. If more compressed data remains after hitting the cap, return a new
Error::SvgzDecompressionLimitReached instead of continuing to decompress
unboundedly. The 1 GiB ceiling is generous for legitimate SVG (plain XML
text), so real-world .svgz files are unaffected.

Also added the corresponding C API error value
(RESVG_ERROR_SVGZ_DECOMPRESSION_LIMIT_REACHED) in crates/c-api/lib.rs,
resvg.h, and ResvgQt.h, appended after the existing values to avoid
changing the numeric value of any existing resvg_error variant (ABI
stability for C/C++ consumers).

Testing

  • Added crates/usvg/src/parser/mod.rs::svgz_tests, unit-testing the new
    limiting logic directly (with a small limit parameter so the tests stay
    fast/cheap rather than allocating a real 1 GiB buffer): normal input under
    the limit decodes correctly, input landing exactly at the limit is
    accepted, and a crafted bomb exceeding the limit is rejected with
    Error::SvgzDecompressionLimitReached.
  • Manually reproduced the bug against the pre-fix code (500 MiB from a
    ~500 KiB input, as described above) and confirmed the fix rejects a
    crafted ~1.5 MiB input that would otherwise decompress to ~1.5 GiB, with
    peak memory now capped at ~1 GiB instead of growing unbounded.
  • Verified a real, legitimate .svgz fixture
    (crates/resvg/tests/resources/image.svgz) still decompresses correctly.
  • cargo test --all --release (full workspace, matching CI's Test step):
    1731 + 3 + 28 + 31 + 1 tests, all passing.
  • cargo fmt --all --check, bash .github/copyright.sh: both pass.
  • cargo build/cargo build --no-default-features in crates/c-api, and
    cargo check --no-default-features in crates/resvg and crates/usvg
    (matching CI's other build steps): all pass.

Scope

Only touches decompress_svgz and the error type it can now return
(propagated through the existing usvg::Error enum and the C API's
resvg_error enum), plus the new tests. No unrelated changes.

Fixes #1138


`decompress_svgz` (used by `Tree::from_data`) called `GzDecoder::read_to_end`
with no bound on the output size. Since GZip allows very high compression
ratios, a tiny, maliciously crafted `.svgz` file can decompress into
gigabytes of data and exhaust all available memory. A ~500 KiB crafted
file was enough to produce 500 MiB in memory in under a second locally.

Cap decompression at 1 GiB and return a new `Error::SvgzDecompressionLimitReached`
(and the matching `RESVG_ERROR_SVGZ_DECOMPRESSION_LIMIT_REACHED` C API
value) when a file would decompress past that limit, instead of reading
to completion unconditionally. Legitimate SVGZ files, which are XML text
and stay well under this size, are unaffected.

Fixes linebender#1138
@akashchamp
akashchamp force-pushed the fix/svgz-decompression-bomb branch from fe52ed9 to ff20ac8 Compare September 25, 2026 18:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Unbounded gzip decompression in usvg::Tree::from_data (SVGZ decompression bomb)

1 participant