DOMPurify, a widely used JavaScript library for sanitizing HTML and preventing cross-site scripting (XSS), has a flaw in its custom attribute handling. Developers who supply an ADD_ATTR predicate function via the EXTRA_ELEMENT_HANDLING.attributeCheck option can accidentally bypass URI validation. If the predicate returns true for a specific attribute-tag pair, the library skips its built-in checks for unsafe protocols like javascript:. This lets malicious links survive sanitization and execute code when clicked.
The vulnerability stems from the _isValidAttribute function. Normally, DOMPurify validates attributes in stages: it checks namespaces, data types, and then URIs for protocols. But when an ADD_ATTR predicate approves an attribute early, the process short-circuits before URI-safe validation kicks in. Attackers craft payloads like <a href="javascript:alert(1)">click</a>. With a predicate that greenlights href on <a> tags, the javascript: URL slips through while other checks pass.
A proof-of-concept demonstrates this. Cantina’s Apex team, who found the issue, built a setup where the predicate accepts href attributes on anchors. They inject the payload into sanitized content, load it in an iframe, and simulate a click. The alert fires, confirming DOM-based XSS execution. This isn’t theoretical—real-world apps using DOMPurify for user-generated content, like forums or CMS plugins, could expose users if they rely on custom predicates.
Vulnerability Mechanics
DOMPurify processes HTML through a parse-then-sanitize pipeline. For attributes, it calls _isValidAttribute, which runs these steps:
- Namespace and prefix validation.
- Custom predicate checks (like ADD_ATTR).
- URI protocol whitelisting if applicable.
If step 2 returns true, it bails out early. No URI check. The library assumes your predicate handles safety, but most developers copy-paste allowlists without reimplementing protocol filters. DOMPurify 2.5.5 and earlier are vulnerable—update to 2.5.6 patches it by enforcing URI validation post-predicate.
// Vulnerable config
const config = {
ADD_ATTR: ['href'], // Or custom predicate
EXTRA_ELEMENT_HANDLING: {
attributeCheck: (nodeName, attName) => attName === 'href' && nodeName === 'A'
}
};
const clean = DOMPurify.sanitize(dirtyHTML, config);
// clean contains javascript:alert(1)
This setup fools the sanitizer. Other attributes like src on images face the same risk if predicates approve them.
Impact and Why It Matters
DOM-based XSS via javascript: URLs accounts for 10-15% of reported XSS flaws in OWASP Top 10 data. DOMPurify powers sanitization in libraries like React’s dangerouslySetInnerHTML wrappers, Angular’s DomSanitizer, and direct use in Node.js/Express apps. Over 1 million weekly npm downloads mean widespread exposure.
Why care? Custom allowlisting tempts devs seeking flexibility—say, permitting data: URIs for avatars or tel: for click-to-call. But skipping URI checks opens doors to phishing, keylogging, or session theft. In iframes or shadow DOM, clicks trigger silently. Skeptically, this isn’t a core lib flaw but a footgun in advanced config. Still, advisories like this highlight how “safe” libs crumble under misuse. Cantina rates it medium (CVSS ~6.1) because it requires predicate use, but that’s common in tailored setups.
Real-world context: Similar bypasses hit HTML Purifier and Esapi in the past. XSS remains the web’s top vector—Veracode’s 2023 report shows it in 26% of apps. If your app parses untrusted HTML, audit configs now.
Mitigation Steps
Upgrade to DOMPurify 2.5.6 immediately—it forces URI validation after predicates. Avoid custom ADD_ATTR if possible; stick to SAFE_FOR_TEMPLATES or ADD_TAGS/ATTR presets. If needed, implement URI checks in your predicate:
attributeCheck: (nodeName, attName, value) => {
if (attName === 'href' && nodeName === 'A') {
return DOMPurify._srcValidElements['A'].indexOf('href') !== -1 &&
/^([a-z]+:)?\/\//.test(value) || value.startsWith('#'); // Basic safe URI
}
return false;
}
Layer defenses: Content-Security-Policy with unsafe-inline blocked, server-side escaping, and CSP nonces. Test with XSS scanners like DOMInvader. Credits to Cantina’s Apex for the find—disclosed responsibly via GitHub.
This underscores a key lesson: No sanitizer is foolproof alone. Combine with input validation and output encoding for real security.