-
Notifications
You must be signed in to change notification settings - Fork 4
fix: sanitize HTML response before DOM injection to prevent XSS #9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,26 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const EVENT_HANDLER_RE = /^on/i; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Parse an HTML string with DOMParser and strip dangerous content | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * (script/iframe/object/embed elements and inline event-handler attributes) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * before it is injected into the document via innerHTML. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function sanitizeHTML(raw: string): string { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const doc = new DOMParser().parseFromString(raw, "text/html"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const dangerous = doc.querySelectorAll("script, iframe, object, embed"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const el of dangerous) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| el.remove(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const all = doc.querySelectorAll("*"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const el of all) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const attr of [...el.attributes]) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (EVENT_HANDLER_RE.test(attr.name)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| el.removeAttribute(attr.name); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return doc.documentElement.innerHTML; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1
to
+25
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const EVENT_HANDLER_RE = /^on/i; | |
| /** | |
| * Parse an HTML string with DOMParser and strip dangerous content | |
| * (script/iframe/object/embed elements and inline event-handler attributes) | |
| * before it is injected into the document via innerHTML. | |
| */ | |
| export function sanitizeHTML(raw: string): string { | |
| const doc = new DOMParser().parseFromString(raw, "text/html"); | |
| const dangerous = doc.querySelectorAll("script, iframe, object, embed"); | |
| for (const el of dangerous) { | |
| el.remove(); | |
| } | |
| const all = doc.querySelectorAll("*"); | |
| for (const el of all) { | |
| for (const attr of [...el.attributes]) { | |
| if (EVENT_HANDLER_RE.test(attr.name)) { | |
| el.removeAttribute(attr.name); | |
| } | |
| } | |
| } | |
| return doc.documentElement.innerHTML; | |
| import DOMPurify from "dompurify"; | |
| /** | |
| * Sanitize an HTML string before it is injected into the document via innerHTML. | |
| * | |
| * Uses DOMPurify with an explicit configuration to strip dangerous elements, | |
| * event-handler attributes, and unsafe URL-bearing attributes/protocols. | |
| */ | |
| export function sanitizeHTML(raw: string): string { | |
| return DOMPurify.sanitize(raw, { | |
| // Explicitly forbid high-risk elements that can change page behavior or | |
| // introduce script execution or redirects. | |
| FORBID_TAGS: ["script", "iframe", "object", "embed", "meta", "base"], | |
| // Forbid particularly risky attributes; DOMPurify also strips `on*` handlers | |
| // and unsafe URL protocols (e.g. `javascript:`) by default. | |
| FORBID_ATTR: ["xlink:href", "formaction"], | |
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
src/browseris excluded from this package’stsctypecheck and ESLint runs, so this new sanitizer logic won’t be statically checked in CI via the existing scripts. Given this is security-sensitive code, it would be safer to add a dedicated browser tsconfig/typecheck step (or include these files) so regressions (e.g., DOM API misuse) are caught early.