Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"CVE-2026-53606"
],
"summary": "sanitize-html has incomplete URI scheme validation in that allows javascript: URIs through action, formaction, data, poster, and background attributes",
"details": "## Summary\n\nsanitize-html uses `allowedSchemesAppliedToAttributes` (default: `['href', 'src', 'cite']`) to gate the `naughtyHref()` function that blocks dangerous URI schemes like `javascript:` and `vbscript:`. The HTML specification defines 10+ attributes that accept URIs (`action`, `formaction`, `data`, `poster`, `background`, `ping`, `xlink:href`, `dynsrc`, `lowsrc`), but none of these are included in the default gate list. When a developer allows any of these attributes in their configuration, `javascript:` URIs pass through completely unmodified, enabling XSS.\n\nThe library has zero awareness of these URI-bearing attributes — none appear anywhere in the 854-line source file (verified by grep). No warning mechanism exists, and the README provides no security guidance about expanding `allowedSchemesAppliedToAttributes` when allowing form or media attributes.\n\n## Severity\n\nExploitation requires non-default configuration: the developer must explicitly allow a non-default tag (e.g., `form`) AND a non-default attribute (e.g., `action`). Default configuration is NOT vulnerable. However, this is a common configuration pattern for CMS platforms, form builders, and rich content editors.\n\n## Affected Versions\n\nAll versions of sanitize-html from v1.18.0 (which introduced `allowedSchemesAppliedToAttributes`) through at least v2.17.2. The default list has been `['href', 'src', 'cite']` since introduction and has never been expanded.\n\n## Root Cause\n\n**File**: `index.js:329` (sanitize-html 2.10.0, confirmed same in 2.17.x)\n\n```javascript\n// Line 329 — The gate that controls scheme validation\nif (options.allowedSchemesAppliedToAttributes.indexOf(a) >= 0) {\n if (naughtyHref(name, value)) {\n delete frame.attribs[a];\n return;\n }\n}\n```\n\n**Default list at line 829**:\n```javascript\nallowedSchemesAppliedToAttributes: ['href', 'src', 'cite'],\n```\n\nThe `naughtyHref()` function (lines 627-667) correctly blocks `javascript:`, `vbscript:`, and other dangerous schemes. However, it has exactly 2 call sites in the entire codebase (lines 330 and 395), both inside the `indexOf` gate. There is no ungated path.\n\nWhen attribute name is `action`, `formaction`, `data`, `poster`, `background`, etc.:\n- `indexOf('action')` returns `-1`\n- The `if` block is skipped entirely\n- `naughtyHref()` is never called\n- `javascript:alert(1)` passes through unmodified\n\nThe `escapeHtml()` function at line 464 provides no defense — it only encodes `& < > \"` characters, which are not present in `javascript:alert(1)`.\n\n**Data Flow**:\n```\nAttacker input: <form action=\"javascript:alert(document.cookie)\">\n1. htmlparser2 parses → tag='form', attribs={action:'javascript:alert(document.cookie)'}\n2. index.js:298 → allowedAttributes check: 'action' in developer config → PASS\n3. index.js:329 → ['href','src','cite'].indexOf('action') → -1 → SKIP naughtyHref()\n4. index.js:464 → escapeHtml('javascript:alert(document.cookie)') → unchanged\n5. OUTPUT: <form action=\"javascript:alert(document.cookie)\">\n```\n\n## Steps to Reproduce\n\n```javascript\nconst sanitize = require('sanitize-html');\n\n// ===== VECTOR 1: form action (100% reliable, all modern browsers) =====\nconst v1 = sanitize(\n '<form action=\"javascript:alert(document.cookie)\"><button>Submit</button></form>',\n {\n allowedTags: ['form', 'button'],\n allowedAttributes: { form: ['action'] }\n }\n);\nconsole.log('V1 (action):', v1);\n// OUTPUT: <form action=\"javascript:alert(document.cookie)\"><button>Submit</button></form>\n// XSS triggers when user submits the form\n\n// ===== VECTOR 2: button formaction (100% reliable) =====\nconst v2 = sanitize(\n '<button formaction=\"javascript:alert(1)\">Click</button>',\n {\n allowedTags: ['button'],\n allowedAttributes: { button: ['formaction'] }\n }\n);\nconsole.log('V2 (formaction):', v2);\n// OUTPUT: <button formaction=\"javascript:alert(1)\">Click</button>\n\n// ===== VECTOR 3: object data =====\nconst v3 = sanitize(\n '<object data=\"javascript:alert(1)\"></object>',\n {\n allowedTags: ['object'],\n allowedAttributes: { object: ['data'] }\n }\n);\nconsole.log('V3 (data):', v3);\n// OUTPUT: <object data=\"javascript:alert(1)\"></object>\n\n// ===== CONTROL: href IS scheme-checked (expected behavior) =====\nconst ctrl = sanitize(\n '<a href=\"javascript:alert(1)\">click</a>',\n {\n allowedTags: ['a'],\n allowedAttributes: { a: ['href'] }\n }\n);\nconsole.log('Control (href):', ctrl);\n// OUTPUT: <a>click</a> ← href correctly stripped by naughtyHref()\n```\n\n**Observed behavior**: `javascript:` preserved on `action`/`formaction`/`data` but correctly stripped on `href`.\n\n**Expected behavior**: `javascript:` should be stripped on ALL URI-bearing attributes, or at minimum, the library should warn developers when they allow URI-bearing attributes not covered by scheme validation.\n\n## Impact\n\nAn attacker can achieve XSS in applications that use sanitize-html with non-default configurations allowing URI-bearing attributes:\n\n- `<form action=\"javascript:...\">` — XSS on form submission (all modern browsers)\n- `<button formaction=\"javascript:...\">` — per-button XSS override (all modern browsers)\n- `<object data=\"javascript:...\">` — object load XSS (Chrome, Firefox)\n- `<video poster=\"javascript:...\">` — limited browser support but spec-valid\n\n**Common vulnerable configurations**:\n- CMS platforms allowing form elements for user-generated content\n- Form builder applications\n- Rich text editors with extended tag allowlists\n- Email template editors allowing media/embed tags\n\n**Mitigating factors**:\n- Default configuration is NOT vulnerable\n- Requires double opt-in: non-default tag + non-default attribute\n- CSP `form-action` directive mitigates form-based vectors\n- Developers CAN manually add attributes to `allowedSchemesAppliedToAttributes`\n\n## Remediation\n\n**Option 1 (Recommended)**: Expand the default `allowedSchemesAppliedToAttributes` list:\n\n```javascript\n// index.js line 829, change from:\nallowedSchemesAppliedToAttributes: ['href', 'src', 'cite'],\n\n// to:\nallowedSchemesAppliedToAttributes: [\n 'href', 'src', 'cite', 'action', 'formaction',\n 'data', 'poster', 'background', 'ping',\n 'xlink:href', 'dynsrc', 'lowsrc'\n],\n```\n\n**Option 2**: Apply `naughtyHref()` to ALL attributes by default (invert the gate logic).\n\n**Option 3**: Add a runtime warning when developers allow URI-bearing attributes not in `allowedSchemesAppliedToAttributes` (analogous to `vulnerableTags` warning for `script`/`style` at lines 124-129).\n\n## Reporter\n\nKevin Lee (Changseon Lee)\nOPCIA Corp. / PeanutAI Inc.\nSeoul, South Korea\nGitHub: crattack",
"details": "## Summary\n\nsanitize-html uses `allowedSchemesAppliedToAttributes` (default: `['href', 'src', 'cite']`) to gate the `naughtyHref()` function that blocks dangerous URI schemes like `javascript:` and `vbscript:`. The HTML specification defines 10+ attributes that accept URIs (`action`, `formaction`, `data`, `poster`, `background`, `ping`, `xlink:href`, `dynsrc`, `lowsrc`), but none of these are included in the default gate list. When a developer allows any of these attributes in their configuration, `javascript:` URIs pass through completely unmodified, enabling XSS.\n\nThe library has zero awareness of these URI-bearing attributes — none appear anywhere in the 854-line source file (verified by grep). No warning mechanism exists, and the README provides no security guidance about expanding `allowedSchemesAppliedToAttributes` when allowing form or media attributes.\n\n## Severity\n\nExploitation requires non-default configuration: the developer must explicitly allow a non-default tag (e.g., `form`) AND a non-default attribute (e.g., `action`). Default configuration is NOT vulnerable. However, this is a common configuration pattern for CMS platforms, form builders, and rich content editors.\n\n## Affected Versions\n\nAll versions of sanitize-html from v1.18.0 (which introduced `allowedSchemesAppliedToAttributes`) through v2.17.4 are affected; v2.17.5 is the first patched release. The default list has been `['href', 'src', 'cite']` since introduction and has never been expanded.\n\n## Root Cause\n\n**File**: `index.js:329` (sanitize-html 2.10.0, confirmed same in 2.17.x)\n\n```javascript\n// Line 329 — The gate that controls scheme validation\nif (options.allowedSchemesAppliedToAttributes.indexOf(a) >= 0) {\n if (naughtyHref(name, value)) {\n delete frame.attribs[a];\n return;\n }\n}\n```\n\n**Default list at line 829**:\n```javascript\nallowedSchemesAppliedToAttributes: ['href', 'src', 'cite'],\n```\n\nThe `naughtyHref()` function (lines 627-667) correctly blocks `javascript:`, `vbscript:`, and other dangerous schemes. However, it has exactly 2 call sites in the entire codebase (lines 330 and 395), both inside the `indexOf` gate. There is no ungated path.\n\nWhen attribute name is `action`, `formaction`, `data`, `poster`, `background`, etc.:\n- `indexOf('action')` returns `-1`\n- The `if` block is skipped entirely\n- `naughtyHref()` is never called\n- `javascript:alert(1)` passes through unmodified\n\nThe `escapeHtml()` function at line 464 provides no defense — it only encodes `& < > \"` characters, which are not present in `javascript:alert(1)`.\n\n**Data Flow**:\n```\nAttacker input: <form action=\"javascript:alert(document.cookie)\">\n1. htmlparser2 parses → tag='form', attribs={action:'javascript:alert(document.cookie)'}\n2. index.js:298 → allowedAttributes check: 'action' in developer config → PASS\n3. index.js:329 → ['href','src','cite'].indexOf('action') → -1 → SKIP naughtyHref()\n4. index.js:464 → escapeHtml('javascript:alert(document.cookie)') → unchanged\n5. OUTPUT: <form action=\"javascript:alert(document.cookie)\">\n```\n\n## Steps to Reproduce\n\n```javascript\nconst sanitize = require('sanitize-html');\n\n// ===== VECTOR 1: form action (100% reliable, all modern browsers) =====\nconst v1 = sanitize(\n '<form action=\"javascript:alert(document.cookie)\"><button>Submit</button></form>',\n {\n allowedTags: ['form', 'button'],\n allowedAttributes: { form: ['action'] }\n }\n);\nconsole.log('V1 (action):', v1);\n// OUTPUT: <form action=\"javascript:alert(document.cookie)\"><button>Submit</button></form>\n// XSS triggers when user submits the form\n\n// ===== VECTOR 2: button formaction (100% reliable) =====\nconst v2 = sanitize(\n '<button formaction=\"javascript:alert(1)\">Click</button>',\n {\n allowedTags: ['button'],\n allowedAttributes: { button: ['formaction'] }\n }\n);\nconsole.log('V2 (formaction):', v2);\n// OUTPUT: <button formaction=\"javascript:alert(1)\">Click</button>\n\n// ===== VECTOR 3: object data =====\nconst v3 = sanitize(\n '<object data=\"javascript:alert(1)\"></object>',\n {\n allowedTags: ['object'],\n allowedAttributes: { object: ['data'] }\n }\n);\nconsole.log('V3 (data):', v3);\n// OUTPUT: <object data=\"javascript:alert(1)\"></object>\n\n// ===== CONTROL: href IS scheme-checked (expected behavior) =====\nconst ctrl = sanitize(\n '<a href=\"javascript:alert(1)\">click</a>',\n {\n allowedTags: ['a'],\n allowedAttributes: { a: ['href'] }\n }\n);\nconsole.log('Control (href):', ctrl);\n// OUTPUT: <a>click</a> ← href correctly stripped by naughtyHref()\n```\n\n**Observed behavior**: `javascript:` preserved on `action`/`formaction`/`data` but correctly stripped on `href`.\n\n**Expected behavior**: `javascript:` should be stripped on ALL URI-bearing attributes, or at minimum, the library should warn developers when they allow URI-bearing attributes not covered by scheme validation.\n\n## Impact\n\nAn attacker can achieve XSS in applications that use sanitize-html with non-default configurations allowing URI-bearing attributes:\n\n- `<form action=\"javascript:...\">` — XSS on form submission (all modern browsers)\n- `<button formaction=\"javascript:...\">` — per-button XSS override (all modern browsers)\n- `<object data=\"javascript:...\">` — object load XSS (Chrome, Firefox)\n- `<video poster=\"javascript:...\">` — limited browser support but spec-valid\n\n**Common vulnerable configurations**:\n- CMS platforms allowing form elements for user-generated content\n- Form builder applications\n- Rich text editors with extended tag allowlists\n- Email template editors allowing media/embed tags\n\n**Mitigating factors**:\n- Default configuration is NOT vulnerable\n- Requires double opt-in: non-default tag + non-default attribute\n- CSP `form-action` directive mitigates form-based vectors\n- Developers CAN manually add attributes to `allowedSchemesAppliedToAttributes`\n\n## Remediation\n\n**Option 1 (Recommended)**: Expand the default `allowedSchemesAppliedToAttributes` list:\n\n```javascript\n// index.js line 829, change from:\nallowedSchemesAppliedToAttributes: ['href', 'src', 'cite'],\n\n// to:\nallowedSchemesAppliedToAttributes: [\n 'href', 'src', 'cite', 'action', 'formaction',\n 'data', 'poster', 'background', 'ping',\n 'xlink:href', 'dynsrc', 'lowsrc'\n],\n```\n\n**Option 2**: Apply `naughtyHref()` to ALL attributes by default (invert the gate logic).\n\n**Option 3**: Add a runtime warning when developers allow URI-bearing attributes not in `allowedSchemesAppliedToAttributes` (analogous to `vulnerableTags` warning for `script`/`style` at lines 124-129).\n\n## Reporter\n\nKevin Lee (Changseon Lee)\nOPCIA Corp. / PeanutAI Inc.\nSeoul, South Korea\nGitHub: crattack",
"severity": [
{
"type": "CVSS_V3",
Expand Down