Vendor dompurify, use isomorphic-dompurify only for tests
This commit is contained in:
@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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")
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { KcSanitizer } from "./KcSanitizer";
|
||||
|
||||
export function kcSanitize(html: string): string {
|
||||
return KcSanitizer.sanitize(html);
|
||||
return KcSanitizer.sanitize(html, {});
|
||||
}
|
||||
|
3
src/lib/vendor/isomorphic-dompurify.ts
vendored
3
src/lib/vendor/isomorphic-dompurify.ts
vendored
@ -1,3 +0,0 @@
|
||||
import DOMPurify from "isomorphic-dompurify";
|
||||
|
||||
export { DOMPurify };
|
Reference in New Issue
Block a user