Vendor dompurify, use isomorphic-dompurify only for tests

This commit is contained in:
Joseph Garrone
2024-09-22 20:12:11 +02:00
parent b6e9043d91
commit ddb0af1dcb
19 changed files with 1370 additions and 72 deletions

View File

@ -1,4 +1,4 @@
import { DOMPurify } from "keycloakify/lib/vendor/isomorphic-dompurify";
import { DOMPurify } from "keycloakify/tools/vendor/dompurify";
type TagType = {
name: string;
@ -22,6 +22,16 @@ export class HtmlPolicyBuilder {
private isStylingAllowed: boolean = false;
private allowedProtocols: Set<string> = new Set();
private enforceRelNofollow: boolean = false;
private DOMPurify: typeof DOMPurify;
// add a constructor
constructor(
dependencyInjections: Partial<{
DOMPurify: typeof DOMPurify;
}>
) {
this.DOMPurify = dependencyInjections.DOMPurify ?? DOMPurify;
}
allowWithoutAttributes(tag: string): this {
this.tagsAllowedWithNoAttribute.add(tag);
@ -69,7 +79,10 @@ export class HtmlPolicyBuilder {
onElements(...tags: string[]): this {
if (this.currentAttribute) {
tags.forEach(tag => {
const element = this.tagsAllowed.get(tag) || { name: tag, attributes: [] };
const element = this.tagsAllowed.get(tag) || {
name: tag,
attributes: []
};
element.attributes.push(this.currentAttribute!);
this.tagsAllowed.set(tag, element);
});
@ -104,10 +117,10 @@ export class HtmlPolicyBuilder {
apply(html: string): string {
//Clear all previous configs first ( in case we used DOMPurify somewhere else )
DOMPurify.clearConfig();
DOMPurify.removeAllHooks();
this.DOMPurify.clearConfig();
this.DOMPurify.removeAllHooks();
this.setupHooks();
return DOMPurify.sanitize(html, {
return this.DOMPurify.sanitize(html, {
ALLOWED_TAGS: Array.from(this.tagsAllowed.keys()),
ALLOWED_ATTR: this.getAllowedAttributes(),
ALLOWED_URI_REGEXP: this.getAllowedUriRegexp(),
@ -118,7 +131,7 @@ export class HtmlPolicyBuilder {
private setupHooks(): void {
// Check allowed attribute and global attributes and it doesnt exist in them remove it
DOMPurify.addHook("uponSanitizeAttribute", (currentNode, hookEvent) => {
this.DOMPurify.addHook("uponSanitizeAttribute", (currentNode, hookEvent) => {
if (!hookEvent) return;
const tagName = currentNode.tagName.toLowerCase();
@ -142,16 +155,24 @@ export class HtmlPolicyBuilder {
currentNode.removeAttribute(hookEvent.attrName);
return;
} else {
const attributeType = allowedAttributes.find(attr => attr.name === hookEvent.attrName);
const attributeType = allowedAttributes.find(
attr => attr.name === hookEvent.attrName
);
if (attributeType) {
//Check if attribute value is allowed
if (attributeType.matchRegex && !attributeType.matchRegex.test(hookEvent.attrValue)) {
if (
attributeType.matchRegex &&
!attributeType.matchRegex.test(hookEvent.attrValue)
) {
hookEvent.forceKeepAttr = false;
hookEvent.keepAttr = false;
currentNode.removeAttribute(hookEvent.attrName);
return;
}
if (attributeType.matchFunction && !attributeType.matchFunction(hookEvent.attrValue)) {
if (
attributeType.matchFunction &&
!attributeType.matchFunction(hookEvent.attrValue)
) {
hookEvent.forceKeepAttr = false;
hookEvent.keepAttr = false;
currentNode.removeAttribute(hookEvent.attrName);
@ -168,9 +189,12 @@ export class HtmlPolicyBuilder {
}
});
DOMPurify.addHook("afterSanitizeAttributes", currentNode => {
this.DOMPurify.addHook("afterSanitizeAttributes", currentNode => {
// if tag is not allowed to have no attribute then remove it completely
if (currentNode.attributes.length == 0 && currentNode.childNodes.length == 0) {
if (
currentNode.attributes.length == 0 &&
currentNode.childNodes.length == 0
) {
if (!this.tagsAllowedWithNoAttribute.has(currentNode.tagName)) {
currentNode.remove();
}
@ -180,7 +204,10 @@ export class HtmlPolicyBuilder {
if (currentNode.attributes.length == 0) {
//add currentNode children to parent node
while (currentNode.firstChild) {
currentNode?.parentNode?.insertBefore(currentNode.firstChild, currentNode);
currentNode?.parentNode?.insertBefore(
currentNode.firstChild,
currentNode
);
}
// Remove the currentNode itself
currentNode.remove();
@ -191,8 +218,13 @@ export class HtmlPolicyBuilder {
if (this.enforceRelNofollow) {
if (!currentNode.hasAttribute("rel")) {
currentNode.setAttribute("rel", "nofollow");
} else if (!currentNode.getAttribute("rel")?.includes("nofollow")) {
currentNode.setAttribute("rel", currentNode.getAttribute("rel") + " nofollow");
} else if (
!currentNode.getAttribute("rel")?.includes("nofollow")
) {
currentNode.setAttribute(
"rel",
currentNode.getAttribute("rel") + " nofollow"
);
}
}
}

View File

@ -1,4 +1,5 @@
import { KcSanitizerPolicy } from "./KcSanitizerPolicy";
import type { DOMPurify as ofTypeDomPurify } from "keycloakify/tools/vendor/dompurify";
// implementation of keycloak java sanitize method ( KeycloakSanitizerMethod )
// https://github.com/keycloak/keycloak/blob/8ce8a4ba089eef25a0e01f58e09890399477b9ef/services/src/main/java/org/keycloak/theme/KeycloakSanitizerMethod.java#L33
@ -6,11 +7,20 @@ export class KcSanitizer {
private static HREF_PATTERN = /\s+href="([^"]*)"/g;
private static textarea: HTMLTextAreaElement | null = null;
public static sanitize(html: string, decodeHtml?: (html: string) => string): string {
public static sanitize(
html: string,
dependencyInjections: Partial<{
DOMPurify: typeof ofTypeDomPurify;
htmlEntitiesDecode: (html: string) => string;
}>
): string {
if (html === "") return "";
html = decodeHtml !== undefined ? decodeHtml(html) : this.decodeHtml(html);
const sanitized = KcSanitizerPolicy.sanitize(html);
html =
dependencyInjections?.htmlEntitiesDecode !== undefined
? dependencyInjections.htmlEntitiesDecode(html)
: this.decodeHtml(html);
const sanitized = KcSanitizerPolicy.sanitize(html, dependencyInjections);
return this.fixURLs(sanitized);
}

View File

@ -1,4 +1,5 @@
import { HtmlPolicyBuilder } from "keycloakify/tools/kcSanitize/HtmlPolicyBuilder";
import { HtmlPolicyBuilder } from "./HtmlPolicyBuilder";
import type { DOMPurify as ofTypeDomPurify } from "keycloakify/tools/vendor/dompurify";
//implementation of java Sanitizer policy ( KeycloakSanitizerPolicy )
// All regex directly copied from the keycloak source but some of them changed slightly to work with typescript(ONSITE_URL and OFFSITE_URL)
@ -76,8 +77,13 @@ export class KcSanitizerPolicy {
);
}
public static sanitize(html: string) {
return new HtmlPolicyBuilder()
public static sanitize(
html: string,
dependencyInjections: Partial<{
DOMPurify: typeof ofTypeDomPurify;
}>
): string {
return new HtmlPolicyBuilder(dependencyInjections)
.allowWithoutAttributes("span")
.allowAttributes("id")

View File

@ -1,5 +1,5 @@
import { KcSanitizer } from "./KcSanitizer";
export function kcSanitize(html: string): string {
return KcSanitizer.sanitize(html);
return KcSanitizer.sanitize(html, {});
}

View File

@ -1,3 +0,0 @@
import DOMPurify from "isomorphic-dompurify";
export { DOMPurify };