{"id":"GHSA-gjj5-9665-rwrc","summary":"probe-image-size: Quadratic-time Denial of Service in the SVG Parser","details":"## Overview\n\n`probe-image-size` scans the SVG header with a searching regular expression, `/\u003c[-_.:a-zA-Z0-9][^\u003e]*\u003e/`. On input that contains many `\u003c` characters but no `\u003e`, the engine restarts the `[^\u003e]*` scan at every `\u003c` position and runs to end of input each time, giving quadratic time complexity.\n\nBoth the synchronous and the streaming parser are affected.\n\n## Impact\n\nEvery entry point that reaches the SVG parser is affected: `probe.sync()`, `probe(stream)` and `probe(url)`. The URL form is the most exposed one — the input is fetched from a remote host, so an attacker only needs to supply a link.\n\nProcessing a crafted buffer blocks the Node.js event loop at 100% CPU for the whole duration. In production environments such as upload validators, image proxies or link unfurl services, a small number of concurrent requests is enough to deny service.\n\n## Root Cause Analysis\n\nTwo independent problems.\n\n1. **Absence of input size cap in the sync path.** `lib/parse_sync/svg.js` copied the entire buffer into a string and matched against it. There was no size limit at all, so cost scaled with the size of the attacker-supplied buffer.\n\n2. **Repeated rescanning in the stream path.** `lib/parse_stream/svg.js` did cap accumulated data at 64 KB, but called `parseSvg(str)` on the whole accumulated string on *every* chunk, giving `O(chunks × N²)`. The cap does not help here: the more chunks the input is split into, the more times the quadratic scan is repeated.\n\n   Chunk size is influenced by the sender. `highWaterMark` (16 KB) is a buffering threshold, not a lower bound — a socket read returns whatever has arrived. A server that writes one byte at a time produces one-byte chunks; this was confirmed against the real `needle` pipeline with default options.\n\nThe original report identified (1) only, and stated that the 64 KB cap mitigates the streaming path. It does not.\n\n## Proof of Concept (PoC)\n\nSynchronous:\n\n```js\nconst probe = require('probe-image-size')\n\n// ~200 KB of '\u003ca' — contains '\u003c' but never '\u003e'\nprobe.sync(Buffer.from('\u003ca'.repeat(100000), 'latin1'))\n```\n\nStreaming — the same payload split into chunks, slower per byte than the synchronous form:\n\n```js\nconst { Readable } = require('stream')\nconst probe = require('probe-image-size')\n\nconst payload = Buffer.from('\u003ca'.repeat(32768), 'latin1')\nconst chunks = []\nfor (let i = 0; i \u003c payload.length; i += 4096) chunks.push(payload.subarray(i, i + 4096))\n\nawait probe(Readable.from(chunks))\n```\n\nMeasurements on the maintainer's machine:\n\n| path | input | time |\n| --- | --- | --- |\n| `probe.sync()` | 25 KB | 0.9 s |\n| `probe.sync()` | 50 KB | 5.5 s |\n| `probe.sync()` | 100 KB | 18 s |\n| `probe.sync()` | 200 KB | 54 s |\n| `probe(stream)` | 64 KB, 1 chunk | 1.6 s |\n| `probe(stream)` | 64 KB, 4 chunks | 2.9 s |\n| `probe(stream)` | 64 KB, 16 chunks | 9.6 s |","aliases":["CVE-2026-104861"],"modified":"2026-10-02T23:30:04.172276888Z","published":"2026-10-02T23:18:02Z","database_specific":{"nvd_published_at":"2026-10-02T18:17:02Z","cwe_ids":["CWE-1333","CWE-400"],"severity":"HIGH","github_reviewed":true,"github_reviewed_at":"2026-10-02T23:18:02Z"},"references":[{"type":"WEB","url":"https://github.com/nodeca/probe-image-size/security/advisories/GHSA-gjj5-9665-rwrc"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-104861"},{"type":"WEB","url":"https://github.com/nodeca/probe-image-size/commit/60cc96ac0b671e79e328213d0a8e831312b09e84"},{"type":"WEB","url":"https://github.com/nodeca/probe-image-size/commit/9b74656d6f973cc59ea2ab1375c0d88390a402ad"},{"type":"WEB","url":"https://github.com/nodeca/probe-image-size/commit/c032aefabdecf5cb50548ab9ba175db56353078f"},{"type":"PACKAGE","url":"https://github.com/nodeca/probe-image-size"},{"type":"WEB","url":"https://github.com/nodeca/probe-image-size/releases/tag/7.4.0"}],"affected":[{"package":{"name":"probe-image-size","ecosystem":"npm","purl":"pkg:npm/probe-image-size"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"7.4.0"}]}],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/10/GHSA-gjj5-9665-rwrc/GHSA-gjj5-9665-rwrc.json","last_known_affected_version_range":"\u003c= 7.3.0"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H"}]}