forked from linode/manager
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor: [M3-7990] - Replace sanitize-html with DOM friendly alterna…
…tive (linode#10378) * Save work * Handle all instances and types * fix tests * build fixes * Trusted types policy * Added changeset: Replace sanitize-html with dompurify
- Loading branch information
1 parent
a68e2f2
commit 5672faf
Showing
10 changed files
with
116 additions
and
145 deletions.
There are no files selected for viewing
5 changes: 5 additions & 0 deletions
5
packages/manager/.changeset/pr-10378-tech-stories-1713297219037.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@linode/manager": Tech Stories | ||
--- | ||
|
||
Replace sanitize-html with dompurify ([#10378](https://github.com/linode/manager/pull/10378)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,77 +1,81 @@ | ||
import sanitize from 'sanitize-html'; | ||
import DOMPurify from 'dompurify'; | ||
|
||
import { allowedHTMLAttr } from 'src/constants'; | ||
|
||
import { getAllowedHTMLTags, isURLValid } from './sanitizeHTML.utils'; | ||
|
||
import type { AllowedHTMLTagsTier } from './sanitizeHTML.utils'; | ||
import type { IOptions } from 'sanitize-html'; | ||
import type { Config } from 'dompurify'; | ||
|
||
type DisallowedTagsMode = 'discard' | 'escape'; | ||
|
||
export interface SanitizeOptions extends Config { | ||
disallowedTagsMode?: DisallowedTagsMode; | ||
} | ||
|
||
interface SanitizeHTMLOptions { | ||
allowMoreTags?: string[]; | ||
disallowedTagsMode?: IOptions['disallowedTagsMode']; | ||
options?: IOptions; | ||
disallowedTagsMode?: DisallowedTagsMode; | ||
sanitizeOptions?: Config; | ||
sanitizingTier: AllowedHTMLTagsTier; | ||
text: string; | ||
} | ||
|
||
export const sanitizeHTML = ({ | ||
allowMoreTags, | ||
disallowedTagsMode = 'escape', | ||
options = {}, | ||
sanitizeOptions: options = {}, | ||
sanitizingTier, | ||
text, | ||
}: SanitizeHTMLOptions) => | ||
sanitize(text, { | ||
allowedAttributes: { | ||
'*': allowedHTMLAttr, | ||
// "target" and "rel" are allowed because they are handled in the | ||
// transformTags map below. | ||
a: [...allowedHTMLAttr, 'target', 'rel'], | ||
}, | ||
allowedClasses: { | ||
span: ['version'], | ||
}, | ||
allowedTags: getAllowedHTMLTags(sanitizingTier, allowMoreTags), | ||
disallowedTagsMode, | ||
transformTags: { | ||
// This transformation function does the following to anchor tags: | ||
// 1. Turns the <a /> into a <span /> if the "href" is invalid | ||
// 2. Adds `rel="noopener noreferrer" if _target is "blank" (for security) | ||
// 3. Removes "target" attribute if it's anything other than "_blank" | ||
// 4. Removes custom "rel" attributes | ||
}: SanitizeHTMLOptions) => { | ||
DOMPurify.setConfig({ | ||
ALLOWED_ATTR: allowedHTMLAttr, | ||
ALLOWED_TAGS: getAllowedHTMLTags(sanitizingTier, allowMoreTags), | ||
KEEP_CONTENT: disallowedTagsMode === 'discard' ? false : true, | ||
RETURN_DOM: false, | ||
RETURN_DOM_FRAGMENT: false, | ||
RETURN_TRUSTED_TYPE: false, | ||
...options, | ||
}); | ||
|
||
a: (tagName, attribs) => { | ||
// If the URL is invalid, transform to a span. | ||
const href = attribs.href ?? ''; | ||
if (href && !isURLValid(href)) { | ||
return { | ||
attribs: {}, | ||
tagName: 'span', | ||
}; | ||
} | ||
// Define transform function for anchor tags | ||
const anchorHandler = (node: HTMLAnchorElement) => { | ||
const href = node.getAttribute('href') ?? ''; | ||
|
||
// If this link opens a new tab, add "noopener noreferrer" for security. | ||
const target = attribs.target ?? ''; | ||
if (target && target === '_blank') { | ||
return { | ||
attribs: { | ||
...attribs, | ||
rel: 'noopener noreferrer', | ||
}, | ||
tagName, | ||
}; | ||
} | ||
// If the URL is invalid, transform to a span. | ||
if (href && !isURLValid(href)) { | ||
const span = document.createElement('span'); | ||
span.setAttribute('class', node.getAttribute('class') || ''); | ||
|
||
// Otherwise we don't want to allow the "rel" or "target" attributes. | ||
delete attribs.rel; | ||
delete attribs.target; | ||
if (node.parentNode) { | ||
node.parentNode.replaceChild(span, node); | ||
} | ||
} else { | ||
// If this link opens a new tab, add "noopener noreferrer" for security. | ||
const target = node.getAttribute('target') || ''; | ||
if (target === '_blank') { | ||
node.setAttribute('rel', 'noopener noreferrer'); | ||
} else { | ||
node.removeAttribute('rel'); | ||
node.removeAttribute('target'); | ||
} | ||
} | ||
}; | ||
|
||
return { | ||
attribs, | ||
tagName, | ||
}; | ||
}, | ||
}, | ||
...options, | ||
}).trim(); | ||
// Register hooks for DOMPurify | ||
DOMPurify.addHook('uponSanitizeElement', (node, data) => { | ||
if (data.tagName === 'a') { | ||
anchorHandler(node as HTMLAnchorElement); | ||
} else if (data.tagName === 'span') { | ||
// Allow class attribute only for span elements | ||
const classAttr = node.getAttribute('class'); | ||
if (classAttr && classAttr.trim() !== 'version') { | ||
node.removeAttribute('class'); | ||
} | ||
} | ||
}); | ||
|
||
// Perform sanitization | ||
const output = DOMPurify.sanitize(text); | ||
return output.trim(); | ||
}; |
Oops, something went wrong.