295 lines
8.8 KiB
TypeScript
295 lines
8.8 KiB
TypeScript
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)
|
|
// 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(
|
|
"\\b(center|left|right|justify|char)\\b",
|
|
"i" // Case-insensitive flag
|
|
);
|
|
|
|
public static readonly VALIGN = new RegExp(
|
|
"\\b(baseline|bottom|middle|top)\\b",
|
|
"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,
|
|
dependencyInjections: Partial<{
|
|
DOMPurify: typeof ofTypeDomPurify;
|
|
}>
|
|
): string {
|
|
return new HtmlPolicyBuilder(dependencyInjections)
|
|
.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);
|
|
}
|
|
}
|