Add KCSantisizer
This commit is contained in:
parent
a5e3ecb38b
commit
c8a31c4b6a
16
package.json
16
package.json
@ -61,9 +61,7 @@
|
|||||||
"bluehats"
|
"bluehats"
|
||||||
],
|
],
|
||||||
"homepage": "https://www.keycloakify.dev",
|
"homepage": "https://www.keycloakify.dev",
|
||||||
"dependencies": {
|
"dependencies": {},
|
||||||
"tsafe": "^1.6.6"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.24.5",
|
"@babel/core": "^7.24.5",
|
||||||
"@babel/generator": "^7.24.5",
|
"@babel/generator": "^7.24.5",
|
||||||
@ -75,7 +73,6 @@
|
|||||||
"@storybook/builder-webpack5": "^6.5.13",
|
"@storybook/builder-webpack5": "^6.5.13",
|
||||||
"@storybook/manager-webpack5": "^6.5.13",
|
"@storybook/manager-webpack5": "^6.5.13",
|
||||||
"@storybook/react": "^6.5.13",
|
"@storybook/react": "^6.5.13",
|
||||||
"eslint-plugin-storybook": "^0.6.7",
|
|
||||||
"@types/babel__generator": "^7.6.4",
|
"@types/babel__generator": "^7.6.4",
|
||||||
"@types/make-fetch-happen": "^10.0.1",
|
"@types/make-fetch-happen": "^10.0.1",
|
||||||
"@types/minimist": "^1.2.2",
|
"@types/minimist": "^1.2.2",
|
||||||
@ -88,6 +85,10 @@
|
|||||||
"cheerio": "1.0.0-rc.12",
|
"cheerio": "1.0.0-rc.12",
|
||||||
"chokidar-cli": "^3.0.0",
|
"chokidar-cli": "^3.0.0",
|
||||||
"cli-select": "^1.1.2",
|
"cli-select": "^1.1.2",
|
||||||
|
"isomorphic-dompurify": "^2.15.0",
|
||||||
|
"tsafe": "^1.6.6",
|
||||||
|
"eslint-plugin-storybook": "^0.6.7",
|
||||||
|
"evt": "^2.5.7",
|
||||||
"husky": "^4.3.8",
|
"husky": "^4.3.8",
|
||||||
"lint-staged": "^11.0.0",
|
"lint-staged": "^11.0.0",
|
||||||
"magic-string": "^0.30.7",
|
"magic-string": "^0.30.7",
|
||||||
@ -103,12 +104,13 @@
|
|||||||
"termost": "^v0.12.1",
|
"termost": "^v0.12.1",
|
||||||
"tsc-alias": "^1.8.10",
|
"tsc-alias": "^1.8.10",
|
||||||
"tss-react": "^4.9.10",
|
"tss-react": "^4.9.10",
|
||||||
|
"tsx": "^4.15.5",
|
||||||
"typescript": "^4.9.4",
|
"typescript": "^4.9.4",
|
||||||
"vite": "^5.2.11",
|
"vite": "^5.2.11",
|
||||||
"vitest": "^1.6.0",
|
"vitest": "^1.6.0",
|
||||||
"yauzl": "^2.10.0",
|
"yauzl": "^2.10.0",
|
||||||
"zod": "^3.17.10",
|
"zod": "^3.17.10",
|
||||||
"evt": "^2.5.7",
|
"html-entities": "2.5.2"
|
||||||
"tsx": "^4.15.5"
|
},
|
||||||
}
|
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||||
}
|
}
|
||||||
|
220
src/tools/kcSanitize/HtmlPolicyBuilder.tsx
Normal file
220
src/tools/kcSanitize/HtmlPolicyBuilder.tsx
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
import DOMPurify from "isomorphic-dompurify";
|
||||||
|
|
||||||
|
type TagType = {
|
||||||
|
name: string;
|
||||||
|
attributes: AttributeType[];
|
||||||
|
};
|
||||||
|
type AttributeType = {
|
||||||
|
name: string;
|
||||||
|
matchRegex?: RegExp;
|
||||||
|
matchFunction?: (value: string) => boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
// implementation for org.owasp.html.HtmlPolicyBuilder
|
||||||
|
// https://www.javadoc.io/static/com.googlecode.owasp-java-html-sanitizer/owasp-java-html-sanitizer/20160628.1/index.html?org/owasp/html/HtmlPolicyBuilder.html
|
||||||
|
// It supports the methods that KCSanitizerPolicy needs and nothing more
|
||||||
|
|
||||||
|
export class HtmlPolicyBuilder {
|
||||||
|
private globalAttributesAllowed: Set<AttributeType> = new Set();
|
||||||
|
private tagsAllowed: Map<string, TagType> = new Map();
|
||||||
|
private tagsAllowedWithNoAttribute: Set<string> = new Set();
|
||||||
|
private currentAttribute: AttributeType | null = null;
|
||||||
|
private isStylingAllowed: boolean = false;
|
||||||
|
private allowedProtocols: Set<string> = new Set();
|
||||||
|
private enforceRelNofollow: boolean = false;
|
||||||
|
|
||||||
|
allowWithoutAttributes(tag: string): this {
|
||||||
|
this.tagsAllowedWithNoAttribute.add(tag);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adds the attributes for validation
|
||||||
|
allowAttributes(...args: string[]): this {
|
||||||
|
if (args.length) {
|
||||||
|
const attr = args[0];
|
||||||
|
this.currentAttribute = { name: attr }; // Default regex, will be set later
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Matching regex for value of allowed attributes
|
||||||
|
matching(matchingPattern: RegExp | ((value: string) => boolean)): this {
|
||||||
|
if (this.currentAttribute) {
|
||||||
|
if (matchingPattern instanceof RegExp) {
|
||||||
|
this.currentAttribute.matchRegex = matchingPattern;
|
||||||
|
} else {
|
||||||
|
this.currentAttribute.matchFunction = matchingPattern;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make attributes in prev call global
|
||||||
|
globally(): this {
|
||||||
|
if (this.currentAttribute) {
|
||||||
|
this.currentAttribute.matchRegex = /.*/;
|
||||||
|
this.globalAttributesAllowed.add(this.currentAttribute);
|
||||||
|
this.currentAttribute = null; // Reset after global application
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow styling globally
|
||||||
|
allowStyling(): this {
|
||||||
|
this.isStylingAllowed = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save attributes for specific tag
|
||||||
|
onElements(...tags: string[]): this {
|
||||||
|
if (this.currentAttribute) {
|
||||||
|
tags.forEach(tag => {
|
||||||
|
const element = this.tagsAllowed.get(tag) || { name: tag, attributes: [] };
|
||||||
|
element.attributes.push(this.currentAttribute!);
|
||||||
|
this.tagsAllowed.set(tag, element);
|
||||||
|
});
|
||||||
|
this.currentAttribute = null; // Reset after applying to elements
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make specific tag allowed
|
||||||
|
allowElements(...tags: string[]): this {
|
||||||
|
tags.forEach(tag => {
|
||||||
|
if (!this.tagsAllowed.has(tag)) {
|
||||||
|
this.tagsAllowed.set(tag, { name: tag, attributes: [] });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle rel=nofollow on links
|
||||||
|
requireRelNofollowOnLinks(): this {
|
||||||
|
this.enforceRelNofollow = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow standard URL protocols (could include further implementation)
|
||||||
|
allowStandardUrlProtocols(): this {
|
||||||
|
this.allowedProtocols.add("http");
|
||||||
|
this.allowedProtocols.add("https");
|
||||||
|
this.allowedProtocols.add("mailto");
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(html: string): string {
|
||||||
|
//Clear all previous configs first ( in case we used DOMPurify somewhere else )
|
||||||
|
DOMPurify.clearConfig();
|
||||||
|
DOMPurify.removeAllHooks();
|
||||||
|
this.setupHooks();
|
||||||
|
return DOMPurify.sanitize(html, {
|
||||||
|
ALLOWED_TAGS: Array.from(this.tagsAllowed.keys()),
|
||||||
|
ALLOWED_ATTR: this.getAllowedAttributes(),
|
||||||
|
ALLOWED_URI_REGEXP: this.getAllowedUriRegexp(),
|
||||||
|
ADD_TAGS: this.isStylingAllowed ? ["style"] : [],
|
||||||
|
ADD_ATTR: this.isStylingAllowed ? ["style"] : []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupHooks(): void {
|
||||||
|
// Check allowed attribute and global attributes and it doesnt exist in them remove it
|
||||||
|
DOMPurify.addHook("uponSanitizeAttribute", (currentNode, hookEvent) => {
|
||||||
|
if (!hookEvent) return;
|
||||||
|
|
||||||
|
const tagName = currentNode.tagName.toLowerCase();
|
||||||
|
const allowedAttributes = this.tagsAllowed.get(tagName)?.attributes || [];
|
||||||
|
|
||||||
|
//Add global attributes to allowed attributes
|
||||||
|
this.globalAttributesAllowed.forEach(attribute => {
|
||||||
|
allowedAttributes.push(attribute);
|
||||||
|
});
|
||||||
|
|
||||||
|
//Add style attribute to allowed attributes
|
||||||
|
if (this.isStylingAllowed) {
|
||||||
|
let styleAttribute: AttributeType = { name: "style", matchRegex: /.*/ };
|
||||||
|
allowedAttributes.push(styleAttribute);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the attribute is allowed
|
||||||
|
if (!allowedAttributes.some(attr => attr.name === hookEvent.attrName)) {
|
||||||
|
hookEvent.forceKeepAttr = false;
|
||||||
|
hookEvent.keepAttr = false;
|
||||||
|
currentNode.removeAttribute(hookEvent.attrName);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
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)) {
|
||||||
|
hookEvent.forceKeepAttr = false;
|
||||||
|
hookEvent.keepAttr = false;
|
||||||
|
currentNode.removeAttribute(hookEvent.attrName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (attributeType.matchFunction && !attributeType.matchFunction(hookEvent.attrValue)) {
|
||||||
|
hookEvent.forceKeepAttr = false;
|
||||||
|
hookEvent.keepAttr = false;
|
||||||
|
currentNode.removeAttribute(hookEvent.attrName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// both attribute and value already checked so they should be ok
|
||||||
|
// set forceKeep to true to make sure next hooks won't delete them
|
||||||
|
// except for href that we will check later
|
||||||
|
if (hookEvent.attrName !== "href") {
|
||||||
|
hookEvent.keepAttr = true;
|
||||||
|
hookEvent.forceKeepAttr = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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 (!this.tagsAllowedWithNoAttribute.has(currentNode.tagName)) {
|
||||||
|
currentNode.remove();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
//in case of <a> or <img> if we have no attribute we need to remove them even if they have child
|
||||||
|
if (currentNode.tagName === "A" || currentNode.tagName === "IMG") {
|
||||||
|
if (currentNode.attributes.length == 0) {
|
||||||
|
//add currentNode children to parent node
|
||||||
|
while (currentNode.firstChild) {
|
||||||
|
currentNode?.parentNode?.insertBefore(currentNode.firstChild, currentNode);
|
||||||
|
}
|
||||||
|
// Remove the currentNode itself
|
||||||
|
currentNode.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//
|
||||||
|
if (currentNode.tagName === "A") {
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private getAllowedAttributes(): string[] {
|
||||||
|
const allowedAttributes: Set<string> = new Set();
|
||||||
|
this.tagsAllowed.forEach(element => {
|
||||||
|
element.attributes.forEach(attribute => {
|
||||||
|
allowedAttributes.add(attribute.name);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this.globalAttributesAllowed.forEach(attribute => {
|
||||||
|
allowedAttributes.add(attribute.name);
|
||||||
|
});
|
||||||
|
return Array.from(allowedAttributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getAllowedUriRegexp(): RegExp {
|
||||||
|
const protocols = Array.from(this.allowedProtocols).join("|");
|
||||||
|
return new RegExp(`^(?:${protocols})://`, "i");
|
||||||
|
}
|
||||||
|
}
|
49
src/tools/kcSanitize/KcSanitizer.ts
Normal file
49
src/tools/kcSanitize/KcSanitizer.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { KcSanitizerPolicy } from "keycloakify/tools/kcSanitize/KcSanitizerPolicy";
|
||||||
|
import { decode } from "html-entities";
|
||||||
|
|
||||||
|
// implementation of keycloak java sanitize method ( KeycloakSanitizerMethod )
|
||||||
|
// https://github.com/keycloak/keycloak/blob/8ce8a4ba089eef25a0e01f58e09890399477b9ef/services/src/main/java/org/keycloak/theme/KeycloakSanitizerMethod.java#L33
|
||||||
|
export class KcSanitizer {
|
||||||
|
private static HREF_PATTERN = /\s+href="([^"]*)"/g;
|
||||||
|
|
||||||
|
public static sanitize(html: string | null): string {
|
||||||
|
if (html == null) {
|
||||||
|
throw new Error("Cannot escape null value.");
|
||||||
|
}
|
||||||
|
if (html === "") return "";
|
||||||
|
|
||||||
|
html = this.decodeHtmlFull(html);
|
||||||
|
const sanitized = KcSanitizerPolicy.sanitize(html);
|
||||||
|
return this.fixURLs(sanitized);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static decodeHtmlFull(html: string): string {
|
||||||
|
return decode(html);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This will remove unwanted characters from url
|
||||||
|
private static fixURLs(msg: string): string {
|
||||||
|
const HREF_PATTERN = this.HREF_PATTERN;
|
||||||
|
const result = [];
|
||||||
|
let last = 0;
|
||||||
|
let match: RegExpExecArray | null;
|
||||||
|
|
||||||
|
do {
|
||||||
|
match = HREF_PATTERN.exec(msg);
|
||||||
|
if (match) {
|
||||||
|
const href = match[0]
|
||||||
|
.replace(/=/g, "=")
|
||||||
|
.replace(/\.\./g, ".")
|
||||||
|
.replace(/&/g, "&");
|
||||||
|
|
||||||
|
result.push(msg.substring(last, match.index!));
|
||||||
|
result.push(href);
|
||||||
|
|
||||||
|
last = HREF_PATTERN.lastIndex;
|
||||||
|
}
|
||||||
|
} while (match);
|
||||||
|
|
||||||
|
result.push(msg.substring(last));
|
||||||
|
return result.join("");
|
||||||
|
}
|
||||||
|
}
|
288
src/tools/kcSanitize/KcSanitizerPolicy.ts
Normal file
288
src/tools/kcSanitize/KcSanitizerPolicy.ts
Normal file
@ -0,0 +1,288 @@
|
|||||||
|
import { HtmlPolicyBuilder } from "keycloakify/tools/kcSanitize/HtmlPolicyBuilder";
|
||||||
|
|
||||||
|
//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)
|
||||||
|
// Also replaced ?i with "i" tag as second parameter of RegExp
|
||||||
|
//https://github.com/keycloak/keycloak/blob/8ce8a4ba089eef25a0e01f58e09890399477b9ef/services/src/main/java/org/keycloak/theme/KeycloakSanitizerPolicy.java#L29
|
||||||
|
export class KcSanitizerPolicy {
|
||||||
|
public static readonly COLOR_NAME = new RegExp(
|
||||||
|
"(?:aqua|black|blue|fuchsia|gray|grey|green|lime|maroon|navy|olive|purple|red|silver|teal|white|yellow)"
|
||||||
|
);
|
||||||
|
|
||||||
|
public static readonly COLOR_CODE = new RegExp(
|
||||||
|
"(?:#(?:[0-9a-fA-F]{3}(?:[0-9a-fA-F]{3})?))"
|
||||||
|
);
|
||||||
|
|
||||||
|
public static readonly NUMBER_OR_PERCENT = new RegExp("[0-9]+%?");
|
||||||
|
|
||||||
|
public static readonly PARAGRAPH = new RegExp(
|
||||||
|
"(?:[\\p{L}\\p{N},'\\.\\s\\-_\\(\\)]|&[0-9]{2};)*",
|
||||||
|
"u" // Unicode flag for \p{L} and \p{N} in the pattern
|
||||||
|
);
|
||||||
|
|
||||||
|
public static readonly HTML_ID = new RegExp("[a-zA-Z0-9\\:\\-_\\.]+");
|
||||||
|
|
||||||
|
public static readonly HTML_TITLE = new RegExp(
|
||||||
|
"[\\p{L}\\p{N}\\s\\-_',:\\[\\]!\\./\\\\\\(\\)&]*",
|
||||||
|
"u" // Unicode flag for \p{L} and \p{N} in the pattern
|
||||||
|
);
|
||||||
|
|
||||||
|
public static readonly HTML_CLASS = new RegExp("[a-zA-Z0-9\\s,\\-_]+");
|
||||||
|
|
||||||
|
public static readonly ONSITE_URL = new RegExp(
|
||||||
|
"(?:[\\p{L}\\p{N}.#@\\$%+&;\\-_~,?=/!]+|#(\\w)+)",
|
||||||
|
"u" // Unicode flag for \p{L} and \p{N} in the pattern
|
||||||
|
);
|
||||||
|
|
||||||
|
public static readonly OFFSITE_URL = new RegExp(
|
||||||
|
"\\s*(?:(?:ht|f)tps?://|mailto:)[\\p{L}\\p{N}]+" +
|
||||||
|
"[\\p{L}\\p{N}\\p{Zs}.#@\\$%+&;:\\-_~,?=/!()]*\\s*",
|
||||||
|
"u" // Unicode flag for \p{L} and \p{N} in the pattern
|
||||||
|
);
|
||||||
|
|
||||||
|
public static readonly NUMBER = new RegExp(
|
||||||
|
"[+-]?(?:(?:[0-9]+(?:\\.[0-9]*)?)|\\.[0-9]+)"
|
||||||
|
);
|
||||||
|
public static readonly NAME = new RegExp("[a-zA-Z0-9\\-_\\$]+");
|
||||||
|
|
||||||
|
public static readonly ALIGN = new RegExp(
|
||||||
|
"center|left|right|justify|char",
|
||||||
|
"i" // Case-insensitive flag
|
||||||
|
);
|
||||||
|
|
||||||
|
public static readonly VALIGN = new RegExp(
|
||||||
|
"baseline|bottom|middle|top",
|
||||||
|
"i" // Case-insensitive flag
|
||||||
|
);
|
||||||
|
|
||||||
|
public static readonly HISTORY_BACK = new RegExp(
|
||||||
|
"(?:javascript:)?\\Qhistory.go(-1)\\E"
|
||||||
|
);
|
||||||
|
|
||||||
|
public static readonly ONE_CHAR = new RegExp(
|
||||||
|
".?",
|
||||||
|
"s" // Dotall flag for . to match newlines
|
||||||
|
);
|
||||||
|
|
||||||
|
private static COLOR_NAME_OR_COLOR_CODE(s: string): boolean {
|
||||||
|
return (
|
||||||
|
KcSanitizerPolicy.COLOR_NAME.test(s) || KcSanitizerPolicy.COLOR_CODE.test(s)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ONSITE_OR_OFFSITE_URL(s: string): boolean {
|
||||||
|
return (
|
||||||
|
KcSanitizerPolicy.ONSITE_URL.test(s) || KcSanitizerPolicy.OFFSITE_URL.test(s)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static sanitize(html: string) {
|
||||||
|
return new HtmlPolicyBuilder()
|
||||||
|
.allowWithoutAttributes("span")
|
||||||
|
|
||||||
|
.allowAttributes("id")
|
||||||
|
.matching(this.HTML_ID)
|
||||||
|
.globally()
|
||||||
|
|
||||||
|
.allowAttributes("class")
|
||||||
|
.matching(this.HTML_CLASS)
|
||||||
|
.globally()
|
||||||
|
|
||||||
|
.allowAttributes("lang")
|
||||||
|
.matching(/[a-zA-Z]{2,20}/)
|
||||||
|
.globally()
|
||||||
|
|
||||||
|
.allowAttributes("title")
|
||||||
|
.matching(this.HTML_TITLE)
|
||||||
|
.globally()
|
||||||
|
|
||||||
|
.allowStyling()
|
||||||
|
|
||||||
|
.allowAttributes("align")
|
||||||
|
.matching(this.ALIGN)
|
||||||
|
.onElements("p")
|
||||||
|
|
||||||
|
.allowAttributes("for")
|
||||||
|
.matching(this.HTML_ID)
|
||||||
|
.onElements("label")
|
||||||
|
|
||||||
|
.allowAttributes("color")
|
||||||
|
.matching(this.COLOR_NAME_OR_COLOR_CODE)
|
||||||
|
.onElements("font")
|
||||||
|
|
||||||
|
.allowAttributes("face")
|
||||||
|
.matching(/[\w;, \-]+/)
|
||||||
|
.onElements("font")
|
||||||
|
|
||||||
|
.allowAttributes("size")
|
||||||
|
.matching(this.NUMBER)
|
||||||
|
.onElements("font")
|
||||||
|
|
||||||
|
.allowAttributes("href")
|
||||||
|
.matching(this.ONSITE_OR_OFFSITE_URL)
|
||||||
|
.onElements("a")
|
||||||
|
|
||||||
|
.allowStandardUrlProtocols()
|
||||||
|
.allowAttributes("nohref")
|
||||||
|
.onElements("a")
|
||||||
|
|
||||||
|
.allowAttributes("name")
|
||||||
|
.matching(this.NAME)
|
||||||
|
.onElements("a")
|
||||||
|
|
||||||
|
.allowAttributes("onfocus", "onblur", "onclick", "onmousedown", "onmouseup")
|
||||||
|
.matching(this.HISTORY_BACK)
|
||||||
|
.onElements("a")
|
||||||
|
|
||||||
|
.requireRelNofollowOnLinks()
|
||||||
|
.allowAttributes("src")
|
||||||
|
.matching(this.ONSITE_OR_OFFSITE_URL)
|
||||||
|
.onElements("img")
|
||||||
|
|
||||||
|
.allowAttributes("name")
|
||||||
|
.matching(this.NAME)
|
||||||
|
.onElements("img")
|
||||||
|
|
||||||
|
.allowAttributes("alt")
|
||||||
|
.matching(this.PARAGRAPH)
|
||||||
|
.onElements("img")
|
||||||
|
|
||||||
|
.allowAttributes("border", "hspace", "vspace")
|
||||||
|
.matching(this.NUMBER)
|
||||||
|
.onElements("img")
|
||||||
|
|
||||||
|
.allowAttributes("border", "cellpadding", "cellspacing")
|
||||||
|
.matching(this.NUMBER)
|
||||||
|
.onElements("table")
|
||||||
|
|
||||||
|
.allowAttributes("bgcolor")
|
||||||
|
.matching(this.COLOR_NAME_OR_COLOR_CODE)
|
||||||
|
.onElements("table")
|
||||||
|
|
||||||
|
.allowAttributes("background")
|
||||||
|
.matching(this.ONSITE_URL)
|
||||||
|
.onElements("table")
|
||||||
|
|
||||||
|
.allowAttributes("align")
|
||||||
|
.matching(this.ALIGN)
|
||||||
|
.onElements("table")
|
||||||
|
|
||||||
|
.allowAttributes("noresize")
|
||||||
|
.matching(new RegExp("noresize", "i"))
|
||||||
|
.onElements("table")
|
||||||
|
|
||||||
|
.allowAttributes("background")
|
||||||
|
.matching(this.ONSITE_URL)
|
||||||
|
.onElements("td", "th", "tr")
|
||||||
|
|
||||||
|
.allowAttributes("bgcolor")
|
||||||
|
.matching(this.COLOR_NAME_OR_COLOR_CODE)
|
||||||
|
.onElements("td", "th")
|
||||||
|
|
||||||
|
.allowAttributes("abbr")
|
||||||
|
.matching(this.PARAGRAPH)
|
||||||
|
.onElements("td", "th")
|
||||||
|
|
||||||
|
.allowAttributes("axis", "headers")
|
||||||
|
.matching(this.NAME)
|
||||||
|
.onElements("td", "th")
|
||||||
|
|
||||||
|
.allowAttributes("scope")
|
||||||
|
.matching(new RegExp("(?:row|col)(?:group)?", "i"))
|
||||||
|
.onElements("td", "th")
|
||||||
|
|
||||||
|
.allowAttributes("nowrap")
|
||||||
|
.onElements("td", "th")
|
||||||
|
|
||||||
|
.allowAttributes("height", "width")
|
||||||
|
.matching(this.NUMBER_OR_PERCENT)
|
||||||
|
.onElements("table", "td", "th", "tr", "img")
|
||||||
|
|
||||||
|
.allowAttributes("align")
|
||||||
|
.matching(this.ALIGN)
|
||||||
|
.onElements(
|
||||||
|
"thead",
|
||||||
|
"tbody",
|
||||||
|
"tfoot",
|
||||||
|
"img",
|
||||||
|
"td",
|
||||||
|
"th",
|
||||||
|
"tr",
|
||||||
|
"colgroup",
|
||||||
|
"col"
|
||||||
|
)
|
||||||
|
|
||||||
|
.allowAttributes("valign")
|
||||||
|
.matching(this.VALIGN)
|
||||||
|
.onElements("thead", "tbody", "tfoot", "td", "th", "tr", "colgroup", "col")
|
||||||
|
|
||||||
|
.allowAttributes("charoff")
|
||||||
|
.matching(this.NUMBER_OR_PERCENT)
|
||||||
|
.onElements("td", "th", "tr", "colgroup", "col", "thead", "tbody", "tfoot")
|
||||||
|
|
||||||
|
.allowAttributes("char")
|
||||||
|
.matching(this.ONE_CHAR)
|
||||||
|
.onElements("td", "th", "tr", "colgroup", "col", "thead", "tbody", "tfoot")
|
||||||
|
|
||||||
|
.allowAttributes("colspan", "rowspan")
|
||||||
|
.matching(this.NUMBER)
|
||||||
|
.onElements("td", "th")
|
||||||
|
|
||||||
|
.allowAttributes("span", "width")
|
||||||
|
.matching(this.NUMBER_OR_PERCENT)
|
||||||
|
.onElements("colgroup", "col")
|
||||||
|
.allowElements(
|
||||||
|
"a",
|
||||||
|
"label",
|
||||||
|
"noscript",
|
||||||
|
"h1",
|
||||||
|
"h2",
|
||||||
|
"h3",
|
||||||
|
"h4",
|
||||||
|
"h5",
|
||||||
|
"h6",
|
||||||
|
"p",
|
||||||
|
"i",
|
||||||
|
"b",
|
||||||
|
"u",
|
||||||
|
"strong",
|
||||||
|
"em",
|
||||||
|
"small",
|
||||||
|
"big",
|
||||||
|
"pre",
|
||||||
|
"code",
|
||||||
|
"cite",
|
||||||
|
"samp",
|
||||||
|
"sub",
|
||||||
|
"sup",
|
||||||
|
"strike",
|
||||||
|
"center",
|
||||||
|
"blockquote",
|
||||||
|
"hr",
|
||||||
|
"br",
|
||||||
|
"col",
|
||||||
|
"font",
|
||||||
|
"map",
|
||||||
|
"span",
|
||||||
|
"div",
|
||||||
|
"img",
|
||||||
|
"ul",
|
||||||
|
"ol",
|
||||||
|
"li",
|
||||||
|
"dd",
|
||||||
|
"dt",
|
||||||
|
"dl",
|
||||||
|
"tbody",
|
||||||
|
"thead",
|
||||||
|
"tfoot",
|
||||||
|
"table",
|
||||||
|
"td",
|
||||||
|
"th",
|
||||||
|
"tr",
|
||||||
|
"colgroup",
|
||||||
|
"fieldset",
|
||||||
|
"legend"
|
||||||
|
)
|
||||||
|
.apply(html);
|
||||||
|
}
|
||||||
|
}
|
114
test/kcSanitize/KcSanitizer.spec.ts
Normal file
114
test/kcSanitize/KcSanitizer.spec.ts
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { KcSanitizer } from "keycloakify/tools/kcSanitize/KcSanitizer";
|
||||||
|
// Adjust the import path as needed
|
||||||
|
|
||||||
|
// Implementation of Keycloak Java method KeycloakSanitizerTest with bunch of more test for p tag styling
|
||||||
|
// https://github.com/keycloak/keycloak/blob/8ce8a4ba089eef25a0e01f58e09890399477b9ef/services/src/test/java/org/keycloak/theme/KeycloakSanitizerTest.java#L32
|
||||||
|
describe("KeycloakSanitizerMethod", () => {
|
||||||
|
it("should handle escapes correctly", () => {
|
||||||
|
let html: string | null = "";
|
||||||
|
let expectedResult: string | null;
|
||||||
|
|
||||||
|
html =
|
||||||
|
"<div class=\"kc-logo-text\"><script>alert('foo');</script><span>Keycloak</span></div>";
|
||||||
|
expectedResult = '<div class="kc-logo-text"><span>Keycloak</span></div>';
|
||||||
|
assertResult(expectedResult, html);
|
||||||
|
|
||||||
|
html = "<h1>Foo</h1>";
|
||||||
|
expectedResult = "<h1>Foo</h1>";
|
||||||
|
assertResult(expectedResult, html);
|
||||||
|
|
||||||
|
html =
|
||||||
|
'<div class="kc-logo-text"><span>Keycloak</span></div><svg onload=alert(document.cookie);>';
|
||||||
|
expectedResult = '<div class="kc-logo-text"><span>Keycloak</span></div>';
|
||||||
|
assertResult(expectedResult, html);
|
||||||
|
|
||||||
|
html = null; // Type assertion to handle null
|
||||||
|
expectedResult = null;
|
||||||
|
expect(() => assertResult(expectedResult, html)).toThrow(
|
||||||
|
"Cannot escape null value."
|
||||||
|
);
|
||||||
|
|
||||||
|
html = "";
|
||||||
|
expectedResult = "";
|
||||||
|
assertResult(expectedResult, html);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle URLs correctly", () => {
|
||||||
|
let html: string = "";
|
||||||
|
|
||||||
|
html = "<p><a href='https://localhost'>link</a></p>";
|
||||||
|
assertResult('<p><a href="https://localhost" rel="nofollow">link</a></p>', html);
|
||||||
|
|
||||||
|
html = '<p><a href="">link</a></p>';
|
||||||
|
assertResult("<p>link</p>", html);
|
||||||
|
|
||||||
|
html = "<p><a href=\"javascript:alert('hello!');\">link</a></p>";
|
||||||
|
assertResult("<p>link</p>", html);
|
||||||
|
|
||||||
|
html = '<p><a href="javascript:alert(document.domain);">link</a></p>';
|
||||||
|
assertResult("<p>link</p>", html);
|
||||||
|
|
||||||
|
html = '<p><a href="javascript:alert(document.domain);">link</a></p>';
|
||||||
|
assertResult("<p>link</p>", html);
|
||||||
|
|
||||||
|
html = '<p><a href="javascript&\0colon;alert(document.domain);">link</a></p>';
|
||||||
|
assertResult("<p>link</p>", html);
|
||||||
|
|
||||||
|
html =
|
||||||
|
'<p><a href="javascript&amp;\0colon;alert(document.domain);">link</a></p>';
|
||||||
|
assertResult("<p>link</p>", html);
|
||||||
|
|
||||||
|
html =
|
||||||
|
'<p><a href="javascript&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;\0colon;alert(document.domain);">link</a></p>';
|
||||||
|
assertResult("<p>link</p>", html);
|
||||||
|
|
||||||
|
html = '<p><a href="https://localhost?key=123&msg=abc">link</a></p>';
|
||||||
|
assertResult(
|
||||||
|
'<p><a href="https://localhost?key=123&msg=abc" rel="nofollow">link</a></p>',
|
||||||
|
html
|
||||||
|
);
|
||||||
|
|
||||||
|
html =
|
||||||
|
"<p><a href='https://localhost?key=123&msg=abc'>link1</a><a href=\"https://localhost?key=abc&msg=123\">link2</a></p>";
|
||||||
|
assertResult(
|
||||||
|
'<p><a href="https://localhost?key=123&msg=abc" rel="nofollow">link1</a><a href="https://localhost?key=abc&msg=123" rel="nofollow">link2</a></p>',
|
||||||
|
html
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it("should handle text styles correctly", () => {
|
||||||
|
let html: string = "";
|
||||||
|
|
||||||
|
html = "<p><strong>text</strong></p>";
|
||||||
|
assertResult("<p><strong>text</strong></p>", html);
|
||||||
|
|
||||||
|
html = "<p><b>text</b></p>";
|
||||||
|
assertResult("<p><b>text</b></p>", html);
|
||||||
|
|
||||||
|
html = `<p class="red"> red text </p>`;
|
||||||
|
assertResult(`<p class="red"> red text </p>`, html);
|
||||||
|
|
||||||
|
html = `<p align="center"> <b>red text </b></p>`;
|
||||||
|
assertResult(`<p align="center"> <b>red text </b></p>`, html);
|
||||||
|
|
||||||
|
html = `<p style="font-size: 20px;">This is a paragraph with larger text.</p>`;
|
||||||
|
assertResult(
|
||||||
|
`<p style="font-size: 20px;">This is a paragraph with larger text.</p>`,
|
||||||
|
html
|
||||||
|
);
|
||||||
|
|
||||||
|
html = `<h3> או נושא שתבחר</h3>`;
|
||||||
|
assertResult(`<h3> או נושא שתבחר</h3>`, html);
|
||||||
|
});
|
||||||
|
|
||||||
|
function assertResult(expectedResult: string | null, html: string | null): void {
|
||||||
|
if (expectedResult === null) {
|
||||||
|
expect(KcSanitizer.sanitize(html)).toThrow("Cannot escape null value.");
|
||||||
|
} else {
|
||||||
|
const result = KcSanitizer.sanitize(html);
|
||||||
|
console.log("expectedResult is ", expectedResult);
|
||||||
|
console.log("Result is ", result);
|
||||||
|
expect(result).toBe(expectedResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user