{"id":"EUVD-2026-105176","revision":null,"records":[{"revision":"582f4cae02654d01a178a3afe6986d53","observedAt":"2026-10-28T15:46:50.301+00:00","statement":{"source":"cve-program","id":"CVE-2026-113650","aliases":[],"title":"ApostropheCMS sanitize-html: unbounded recursion when re-sanitizing the fallback of a discarded iframe (stack overflow, nestingLimit not enforced)","description":"ApostropheCMS is an open-source Node.js content management system. In sanitize-html 2.18.0, when an <iframe> is not allowed and disallowedTagsMode is 'discard' (the defaults), its raw-text content is re-sanitized by a recursive call to sanitizeHtml(text, options). The recursion has no bound and starts each nested call with a fresh state. This vulnerability is fixed in 2.18.1.","state":0,"publishedAt":"2026-10-20T16:19:12.1+00:00","modifiedAt":"2026-10-28T14:02:38.5+00:00","affected":[{"ecosystem":"Vendor product","name":"apostrophe","ranges":[],"versions":[],"rawVersionEvidence":"{\n                    \"vendor\": \"apostrophecms\",\n                    \"product\": \"apostrophe\",\n                    \"versions\": [\n                        {\n                            \"version\": \"< 4.34.0\",\n                            \"status\": \"affected\"\n                        }\n                    ]\n                }"},{"ecosystem":"Vendor product","name":"sanitize-html","ranges":[],"versions":[],"rawVersionEvidence":"{\n                    \"vendor\": \"apostrophecms\",\n                    \"product\": \"sanitize-html\",\n                    \"versions\": [\n                        {\n                            \"version\": \">= 2.18.0, < 2.18.1\",\n                            \"status\": \"affected\"\n                        }\n                    ]\n                }"}],"metrics":[{"system":"CVSS 3.1","vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L","score":5.3,"severity":"MEDIUM"}],"references":["https://github.com/apostrophecms/apostrophe/security/advisories/GHSA-c9rx-x947-fp3h","https://github.com/apostrophecms/apostrophe/commit/06e4b377f3c360f9e89c32dccd7e505ad5ddf498","https://github.com/apostrophecms/apostrophe/commit/40b989b1a9dd23d30029b8ce7668634265cfe5ea","https://github.com/apostrophecms/apostrophe/releases/tag/sanitize-html@2.18.1"],"sourceUrl":"https://www.cve.org/CVERecord?id=CVE-2026-113650","license":"CVE Program Terms of Use","key":"cve-program:CVE-2026-113650","identifiers":["CVE-2026-113650"]},"retained":false,"historicalAffected":[]},{"revision":"c8cd8769209f43f58002ded1e6287915","observedAt":"2026-10-30T01:57:25.167+00:00","statement":{"source":"euvd","id":"EUVD-2026-105176","aliases":["CVE-2026-113650","GHSA-c9rx-x947-fp3h"],"title":"ApostropheCMS is an open-source Node.js content management system. In sanitize-html 2.18.0, when an <iframe> is not allowed and disallowedTagsMode is 'discard' (the defaults), its raw-text content is re-sanitized by a recursive call to sani…","description":"ApostropheCMS is an open-source Node.js content management system. In sanitize-html 2.18.0, when an <iframe> is not allowed and disallowedTagsMode is 'discard' (the defaults), its raw-text content is re-sanitized by a recursive call to sanitizeHtml(text, options). The recursion has no bound and starts each nested call with a fresh state. This vulnerability is fixed in 2.18.1.","state":0,"publishedAt":null,"modifiedAt":null,"affected":[],"metrics":[{"system":"EUVD CVSS 3.1","vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L","score":5.3,"severity":null}],"references":["https://github.com/apostrophecms/apostrophe/security/advisories/GHSA-c9rx-x947-fp3h","https://github.com/apostrophecms/apostrophe/commit/06e4b377f3c360f9e89c32dccd7e505ad5ddf498","https://github.com/apostrophecms/apostrophe/commit/40b989b1a9dd23d30029b8ce7668634265cfe5ea","https://github.com/apostrophecms/apostrophe/releases/tag/sanitize-html@2.18.1"],"sourceUrl":"https://euvd.enisa.europa.eu/vulnerability/EUVD-2026-105176","license":"ENISA EUVD · source terms and attribution apply","enrichment":{"sourceStatus":null,"weaknesses":[],"productCriteria":[],"sourceDates":{"datePublished":"Oct 20, 2026, 4:19:12 PM","dateUpdated":"Oct 28, 2026, 2:02:38 PM"},"relatedAdvisories":[],"note":"EUVD source date text has no stated timezone and is retained without inventing a UTC instant. Related bulletin membership is not an alias, common cause or applicability claim. A missing vector with a zero score is treated as unrated. Complete vendor/product and underlying source evidence remains in the original JSON."},"key":"euvd:EUVD-2026-105176","identifiers":["EUVD-2026-105176","CVE-2026-113650","GHSA-c9rx-x947-fp3h"]},"retained":false,"historicalAffected":[]},{"revision":"284b5ac7863943ed952d0ec73895243f","observedAt":"2026-10-29T06:48:04.607+00:00","statement":{"source":"nvd","id":"CVE-2026-113650","aliases":[],"title":"ApostropheCMS is an open-source Node.js content management system. In sanitize-html 2.18.0, when an <iframe> is not allowed and disallowedTagsMode is 'discard' (the defaults), its raw-text content is re-sanitized by a recursive call to sani…","description":"ApostropheCMS is an open-source Node.js content management system. In sanitize-html 2.18.0, when an <iframe> is not allowed and disallowedTagsMode is 'discard' (the defaults), its raw-text content is re-sanitized by a recursive call to sanitizeHtml(text, options). The recursion has no bound and starts each nested call with a fresh state. This vulnerability is fixed in 2.18.1.","state":0,"publishedAt":"2026-10-20T17:17:35.807+00:00","modifiedAt":"2026-10-28T14:16:56.61+00:00","affected":[],"metrics":[{"system":"CVSS 3.1 · security-advisories@github.com · Secondary","vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L","score":5.3,"severity":"MEDIUM"}],"references":["https://github.com/apostrophecms/apostrophe/commit/06e4b377f3c360f9e89c32dccd7e505ad5ddf498","https://github.com/apostrophecms/apostrophe/commit/40b989b1a9dd23d30029b8ce7668634265cfe5ea","https://github.com/apostrophecms/apostrophe/releases/tag/sanitize-html@2.18.1","https://github.com/apostrophecms/apostrophe/security/advisories/GHSA-c9rx-x947-fp3h"],"sourceUrl":"https://nvd.nist.gov/vuln/detail/CVE-2026-113650","license":"NIST NVD usage terms; underlying source attribution retained","enrichment":{"sourceStatus":"Deferred","weaknesses":["CWE-674"],"productCriteria":[],"sourceDates":{"published":"2026-10-20T17:17:35.807","lastModified":"2026-10-28T14:16:56.610"},"relatedAdvisories":[],"note":"NVD analysis and CPE criteria are source context, not proof that a package or your environment is affected. Complete affected data, logical configurations and SSVC are retained in the original JSON."},"key":"nvd:CVE-2026-113650","identifiers":["CVE-2026-113650"]},"retained":false,"historicalAffected":[]},{"revision":"475b0358081b4b40bf1fa81c7a063066","observedAt":"2026-10-24T13:17:17.793+00:00","statement":{"source":"osv","id":"GHSA-c9rx-x947-fp3h","aliases":["CVE-2026-113650"],"title":"ApostropheCMS sanitize-html: unbounded recursion when re-sanitizing the fallback of a discarded iframe (stack overflow, nestingLimit not enforced)","description":"### Summary\n\nSince 2.18.0, when an `<iframe>` is not allowed and `disallowedTagsMode` is `'discard'` (the defaults),\nits raw-text content is re-sanitized by a recursive call to `sanitizeHtml(text, options)` (the change\nfrom #5564, fixing #5550). The recursion has no bound and starts each nested call with a fresh state.\nTwo consequences, both absent from 2.17.7 and 2.17.4:\n\n1. A few kilobytes of unclosed `<iframe>` tags throw `RangeError: Maximum call stack size exceeded`\n   with the default options. Any application that sanitizes untrusted HTML with 2.18.0 can be made to\n   throw on a small crafted input.\n2. `nestingLimit` is not enforced across a discarded iframe: each nested iframe restarts the depth\n   count at zero, so the output can be nested arbitrarily deep regardless of the configured limit.\n\n### Affected versions\n\n- 2.18.0 (the current `latest`), and the `main` branch at the time of writing (the recursive call is\n  still in `packages/sanitize-html/index.js`, in the `ontext` handler, branch\n  `tag === 'iframe' && !tagAllowed(tag) && options.disallowedTagsMode === 'discard'`).\n- Not affected: 2.17.7 and 2.17.4 (tested with the same inputs below).\n\nTested with Node.js v26.7.0.\n\n### Reproduction 1: stack overflow with the default options\n\n```js\nconst sanitizeHtml = require('sanitize-html');\n\n// with 2.18.0:\nsanitizeHtml('<iframe>'.repeat(1000));  // 8,000 chars: returns ''\nsanitizeHtml('<iframe>'.repeat(2000));  // 16,000 chars: throws RangeError: Maximum call stack size exceeded\nsanitizeHtml('<iframe>'.repeat(20000)); // 160,000 chars: throws RangeError: Maximum call stack size exceeded\n```\n\nResults:\n\n| input | 2.17.4 | 2.17.7 | 2.18.0 |\n|---|---|---|---|\n| `'<iframe>'.repeat(1000)` | ok | ok | ok |\n| `'<iframe>'.repeat(2000)` | ok | ok | RangeError |\n| `'<iframe>'.repeat(20000)` | ok | ok | RangeError |\n\nThe threshold depends on the remaining stack, so it is lower when `sanitizeHtml` is called from an\nalready deep call stack (for example inside a server-side rendering pass).\n\n### Reproduction 2: `nestingLimit` is bypassed\n\n```js\nconst sanitizeHtml = require('sanitize-html');\n\nconst input = ('<iframe>' + '<b>'.repeat(3)).repeat(50) + 'x';\nconst out = sanitizeHtml(input, { allowedTags: ['b'], nestingLimit: 3 });\n// maximum nesting depth of <b> in `out`:\n//   2.17.4: 2    2.17.7: 0 (fallback escaped as text)    2.18.0: 150\n```\n\nWithout an iframe, the same options cap the output at three nested `<b>` in all three versions. With\n`k` repetitions of `'<iframe>' + '<b>'.repeat(3)`, 2.18.0 returns `3 * k` nested `<b>` (measured for\nk = 1, 5, 50: depth 3, 15, 150).\n\n### Cause\n\nIn 2.18.0 the `ontext` handler does:\n\n```js\n} else if (\n  tag === 'iframe' &&\n  !tagAllowed(tag) &&\n  options.disallowedTagsMode === 'discard'\n) {\n  result += sanitizeHtml(text, options);\n}\n```\n\nBecause htmlparser2 treats `<iframe>` as a raw-text element, the text of an unclosed iframe is the whole\nremainder of the input; re-parsing it can meet another unclosed `<iframe>`, and so on, one stack frame\n(and one full re-parse) per iframe. The nested call also starts with its own `depth`, so tags inside the\nfallback are not counted against `nestingLimit` together with the enclosing tags.\n\n### Possible directions\n\nNot a patch, only what would close both effects: bound the recursion (or process the fallback without\nrecursing), and carry the current depth into the nested sanitization so that `nestingLimit` applies to\nthe whole output.","state":0,"publishedAt":"2026-10-21T17:05:01+00:00","modifiedAt":"2026-10-21T17:15:08.6348853+00:00","affected":[{"ecosystem":"npm","name":"sanitize-html","ranges":[{"type":"SEMVER","events":[{"kind":"introduced","version":"2.18.0"},{"kind":"fixed","version":"2.18.1"}]}],"versions":[],"rawVersionEvidence":null}],"metrics":[{"system":"CVSS_V3","vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L","score":null,"severity":null},{"system":"Publisher rating","vector":null,"score":null,"severity":"MODERATE"}],"references":["https://github.com/apostrophecms/apostrophe/security/advisories/GHSA-c9rx-x947-fp3h","https://nvd.nist.gov/vuln/detail/CVE-2026-113650","https://github.com/apostrophecms/apostrophe/commit/06e4b377f3c360f9e89c32dccd7e505ad5ddf498","https://github.com/apostrophecms/apostrophe/commit/40b989b1a9dd23d30029b8ce7668634265cfe5ea","https://github.com/apostrophecms/apostrophe","https://github.com/apostrophecms/apostrophe/releases/tag/sanitize-html@2.18.1"],"sourceUrl":"https://github.com/advisories/GHSA-c9rx-x947-fp3h","license":"GitHub Advisory Database · CC-BY-4.0","key":"osv:GHSA-c9rx-x947-fp3h","identifiers":["GHSA-c9rx-x947-fp3h","CVE-2026-113650"]},"retained":false,"historicalAffected":[]}],"coverage":[{"source":"cisa-kev:global","scope":"Complete CISA KEV catalog; all vendors. Missing entries do not establish absence of exploitation.","generation":"2026-10-31T07:57:38.5116842+00:00","complete":true,"records":1766,"lastAttemptAt":"2026-10-31T07:57:38.5116842+00:00","lastSuccessAt":"2026-10-31T07:57:38.6739729+00:00","error":null,"lastVerifiedAt":"2026-10-31T07:57:38.6739739+00:00","snapshotPublishedAt":null,"importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"cve-program","scope":"Official CVE Program baseline and cumulative hourly delta; all vendors, published and rejected records","generation":"cve-9266d7b009bf9c4a0ccdcb5cbdea7f905cb3c4d92fd3b41581ece16b2cfdc4e7-96726b0429e12f7420216866e04292e25250b20c8e6290ae6ff1799519d62cd3","complete":true,"records":417534,"lastAttemptAt":"2026-10-31T07:57:37.9765891+00:00","lastSuccessAt":"2026-10-31T07:57:37.9373669+00:00","error":null,"lastVerifiedAt":"2026-10-31T07:57:37.9765891+00:00","snapshotPublishedAt":"2026-10-31T07:26:11+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"euvd","scope":"Rolling EUVD lookups of CVE aliases in current public OSV archives; 500 identifiers per refresh, rechecked after 48 hours. Not a global EUVD catalog. Successful not-found lookups count as checked; they do not imply withdrawal or safety. No reader inventory is sent.","generation":"enrichment-c5eb069e059248ab9c1b8cc754ce0885","complete":true,"records":13565,"lastAttemptAt":"2026-10-31T07:59:26.0749145+00:00","lastSuccessAt":"2026-10-31T03:47:38.206+00:00","error":null,"lastVerifiedAt":"2026-10-31T03:47:38.206+00:00","snapshotPublishedAt":null,"importing":false,"stagedRecords":0,"checkedIdentifiers":13637,"expectedIdentifiers":13637,"failedIdentifiers":0},{"source":"first-epss:nuget","scope":"FIRST EPSS for explicit CVE aliases in the collected public NuGet archive. Not a global EPSS backfill.","generation":"2026-10-31T07:57:38.5116842+00:00","complete":true,"records":1094,"lastAttemptAt":"2026-10-31T07:57:38.5116842+00:00","lastSuccessAt":"2026-10-31T07:57:38.6750663+00:00","error":null,"lastVerifiedAt":"2026-10-31T07:57:38.6750666+00:00","snapshotPublishedAt":null,"importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"nvd","scope":"Rolling NVD lookups of CVE aliases in current public OSV archives; 500 identifiers per refresh, rechecked after 48 hours. Not a global NVD catalog. Successful not-found lookups count as checked; they do not imply withdrawal or safety. No reader inventory is sent.","generation":"enrichment-ea58f9c685fb45208f84e40f4ddb9154","complete":false,"records":13577,"lastAttemptAt":"2026-10-31T07:59:25.4673363+00:00","lastSuccessAt":"2026-10-31T07:59:25.464+00:00","error":null,"lastVerifiedAt":"2026-10-31T07:59:25.464+00:00","snapshotPublishedAt":null,"importing":false,"stagedRecords":0,"checkedIdentifiers":13547,"expectedIdentifiers":13637,"failedIdentifiers":0},{"source":"osv:Go","scope":"Complete OSV Go ecosystem archive, including withdrawn and malicious-package source records; statement counts are not unique CVE counts","generation":"go-21dcd6662970dfea02e4ea60ee637115e8d92664947484d2b9276b3f6568a84f","complete":true,"records":9815,"lastAttemptAt":"2026-10-31T07:57:38.3405761+00:00","lastSuccessAt":"2026-10-30T23:47:34.1817587+00:00","error":null,"lastVerifiedAt":"2026-10-31T07:57:38.3405761+00:00","snapshotPublishedAt":"2026-10-30T23:33:17+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"osv:NuGet","scope":"Complete OSV NuGet ecosystem archive, including GitHub Advisory Database records; NuGet version matching only","generation":"nuget-2de9ccc5290f060c7aa63f89c25dd76f655a64ba1aba7c05c97fa3bee4f09899","complete":true,"records":1967,"lastAttemptAt":"2026-10-31T07:57:38.1313492+00:00","lastSuccessAt":"2026-10-28T20:55:34.2982937+00:00","error":null,"lastVerifiedAt":"2026-10-31T07:57:38.1313492+00:00","snapshotPublishedAt":"2026-10-28T20:03:08+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"osv:crates.io","scope":"Complete OSV crates.io ecosystem archive, including withdrawn and malicious-package source records; statement counts are not unique CVE counts","generation":"crates.io-f95f76cd406ae5bae3de632ad37fd2d5160b44f5e6b51748ac6f8f0c0c88e72b","complete":true,"records":3028,"lastAttemptAt":"2026-10-31T07:57:38.4524705+00:00","lastSuccessAt":"2026-10-30T23:47:38.4914494+00:00","error":null,"lastVerifiedAt":"2026-10-31T07:57:38.4524705+00:00","snapshotPublishedAt":"2026-10-30T23:18:12+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"osv:npm","scope":"Complete OSV npm ecosystem archive, including withdrawn and malicious-package source records; statement counts are not unique CVE counts","generation":"npm-3ce2e3336768d0abd5b020f6f12bd5502a5deb02700e9d89d279597dce6e72ae","complete":true,"records":230992,"lastAttemptAt":"2026-10-31T07:57:38.240628+00:00","lastSuccessAt":"2026-10-31T03:47:08.0119269+00:00","error":null,"lastVerifiedAt":"2026-10-31T07:57:38.240628+00:00","snapshotPublishedAt":"2026-10-31T03:03:11+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null}]}