{"schema":"threatline.record/2","record":{"id":"CVE-2026-104861","aliases":["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, `/<[-_.:a-zA-Z0-9][^>]*>/`. On input that contains many `<` characters but no `>`, the engine restarts the `[^>]*` scan at every `<` 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 '<a' — contains '<' but never '>'\nprobe.sync(Buffer.from('<a'.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('<a'.repeat(32768), 'latin1')\nconst chunks = []\nfor (let i = 0; i < 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 |","categories":["Libraries & packages"],"product":"probe-image-size","vendor":"nodeca","ecosystem":"npm","affected_ranges":[{"source":"GitHub Advisory Database","source_url":"https://github.com/advisories/GHSA-gjj5-9665-rwrc","source_modified_at":"2026-10-02T23:18:02Z","fetched_at":"2026-10-23T18:42:43.628194Z","package":{"ecosystem":"npm","name":"probe-image-size"},"range":"<= 7.3.0"},{"source":"CVE List","source_url":"https://www.cve.org/CVERecord?id=CVE-2026-104861","source_modified_at":"2026-10-02T18:20:12.054Z","fetched_at":"2026-10-23T18:45:31.882637Z","product":"probe-image-size","vendor":"nodeca","versions":[{"version":"< 7.4.0","status":"affected"}],"defaultStatus":null},{"source":"OSV","source_url":"https://osv.dev/vulnerability/GHSA-gjj5-9665-rwrc","source_modified_at":"2026-10-02T23:30:06.744851556Z","fetched_at":"2026-10-23T18:45:31.882637Z","package":{"name":"probe-image-size","ecosystem":"npm","purl":"pkg:npm/probe-image-size"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"7.4.0"}]}],"versions":[]}],"fixed_versions":[{"source":"GitHub Advisory Database","source_url":"https://github.com/advisories/GHSA-gjj5-9665-rwrc","source_modified_at":"2026-10-02T23:18:02Z","fetched_at":"2026-10-23T18:42:43.628194Z","package":{"ecosystem":"npm","name":"probe-image-size"},"version":"7.4.0"},{"source":"OSV","source_url":"https://osv.dev/vulnerability/GHSA-gjj5-9665-rwrc","source_modified_at":"2026-10-02T23:30:06.744851556Z","fetched_at":"2026-10-23T18:45:31.882637Z","package":{"name":"probe-image-size","ecosystem":"npm","purl":"pkg:npm/probe-image-size"},"version":"7.4.0"}],"cvss":7.5,"severity":"High","kev":{"listed":false},"epss":{"probability":0.00425,"percentile":0.35749,"score_date":"2026-10-23"},"published_at":"2026-10-02T17:56:15.515Z","modified_at":"2026-10-02T23:30:06.744851556Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/advisories/GHSA-gjj5-9665-rwrc","fetched_at":"2026-10-23T18:42:43.628194Z","source_modified_at":"2026-10-02T23:18:02Z"},{"source":"CVE List","url":"https://www.cve.org/CVERecord?id=CVE-2026-104861","fetched_at":"2026-10-23T18:45:31.882637Z","source_modified_at":"2026-10-02T18:20:12.054Z"},{"source":"OSV","url":"https://osv.dev/vulnerability/GHSA-gjj5-9665-rwrc","fetched_at":"2026-10-23T18:45:31.882637Z","source_modified_at":"2026-10-02T23:30:06.744851556Z"},{"source":"EPSS","url":"https://api.first.org/data/v1/epss?cve=CVE-2026-104861","fetched_at":"2026-10-23T18:42:43.628194Z","source_modified_at":"2026-10-23"}],"sources":["GitHub Advisory Database","CVE List","OSV","EPSS"],"references":[{"source":"CVE List","url":"https://github.com/nodeca/probe-image-size/security/advisories/GHSA-gjj5-9665-rwrc","tags":["exploit"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-104861","source":"GitHub Advisory Database"},{"source":"CVE List","url":"https://github.com/nodeca/probe-image-size/commit/60cc96ac0b671e79e328213d0a8e831312b09e84","tags":["x_refsource_MISC"]},{"source":"CVE List","url":"https://github.com/nodeca/probe-image-size/commit/9b74656d6f973cc59ea2ab1375c0d88390a402ad","tags":["x_refsource_MISC"]},{"source":"CVE List","url":"https://github.com/nodeca/probe-image-size/commit/c032aefabdecf5cb50548ab9ba175db56353078f","tags":["x_refsource_MISC"]},{"source":"CVE List","url":"https://github.com/nodeca/probe-image-size/releases/tag/7.4.0","tags":["x_refsource_MISC"]},{"url":"https://github.com/advisories/GHSA-gjj5-9665-rwrc","source":"GitHub Advisory Database"}],"locations":[],"motoko_assessment":{"status":"requires_architecture_review","automatic_remediation":false},"cve_state":"PUBLISHED","ssvc":{"exploitation":"poc","automatable":"yes","technical_impact":"partial","assessed_at":"2026-10-02T18:19:55.305774Z"},"classification":{"method":"product-first software-layer rules","rules_version":"2.1","matched_on":"ecosystem","confidence":"heuristic","reviewed":false},"change_timestamp":"2026-10-02T23:30:06.744851556Z","content_hash":"9b7bfddbfe33352e57b90c8e3939d3af39dda4959da8d1a270d80bd112620225"}}
