use textarea on client for decode
This commit is contained in:
parent
7e6a84ce19
commit
66b480f837
@ -1,26 +1,44 @@
|
||||
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;
|
||||
private static textarea: HTMLTextAreaElement | null = null;
|
||||
|
||||
public static sanitize(html: string | null): string {
|
||||
public static async sanitize(html: string | null): Promise<string> {
|
||||
if (html == null) {
|
||||
throw new Error("Cannot escape null value.");
|
||||
}
|
||||
if (html === "") return "";
|
||||
|
||||
html = this.decodeHtmlFull(html);
|
||||
html = await this.decodeHtmlFull(html);
|
||||
const sanitized = KcSanitizerPolicy.sanitize(html);
|
||||
return this.fixURLs(sanitized);
|
||||
}
|
||||
|
||||
private static decodeHtmlFull(html: string): string {
|
||||
private static async decodeHtmlFull(html: string): Promise<string> {
|
||||
if (typeof window !== "undefined" && typeof document !== "undefined") {
|
||||
return KcSanitizer.decodeHtmlOnClient(html);
|
||||
} else {
|
||||
return await KcSanitizer.decodeHtmlOnServer(html);
|
||||
}
|
||||
}
|
||||
|
||||
private static async decodeHtmlOnServer(html: string): Promise<string> {
|
||||
// Dynamically import html-entities only on the server side
|
||||
const { decode } = await import("html-entities");
|
||||
return decode(html);
|
||||
}
|
||||
|
||||
private static decodeHtmlOnClient(html: string): string {
|
||||
if (!KcSanitizer.textarea) {
|
||||
KcSanitizer.textarea = document.createElement("textarea");
|
||||
}
|
||||
KcSanitizer.textarea.innerHTML = html;
|
||||
return KcSanitizer.textarea.value;
|
||||
}
|
||||
|
||||
// This will remove unwanted characters from url
|
||||
private static fixURLs(msg: string): string {
|
||||
const HREF_PATTERN = this.HREF_PATTERN;
|
||||
|
@ -1,146 +1,228 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { describe, it, expect, vi, beforeAll } from "vitest";
|
||||
import { KcSanitizer } from "keycloakify/tools/kcSanitize/KcSanitizer";
|
||||
|
||||
// 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(
|
||||
const testCases = [
|
||||
{
|
||||
description: "should handle escapes correctly",
|
||||
cases: [
|
||||
{
|
||||
html: "<div class=\"kc-logo-text\"><script>alert('foo');</script><span>Keycloak</span></div>",
|
||||
expectedResult: '<div class="kc-logo-text"><span>Keycloak</span></div>'
|
||||
},
|
||||
{
|
||||
html: "<h1>Foo</h1>",
|
||||
expectedResult: "<h1>Foo</h1>"
|
||||
},
|
||||
{
|
||||
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>'
|
||||
},
|
||||
{
|
||||
html: null,
|
||||
expectedResult: null
|
||||
},
|
||||
{
|
||||
html: "",
|
||||
expectedResult: ""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
description: "should handle URLs correctly",
|
||||
cases: [
|
||||
{
|
||||
html: "<p><a href='https://localhost'>link</a></p>",
|
||||
expectedResult:
|
||||
'<p><a href="https://localhost" rel="nofollow">link</a></p>'
|
||||
},
|
||||
{
|
||||
html: '<p><a href="">link</a></p>',
|
||||
expectedResult: "<p>link</p>"
|
||||
},
|
||||
{
|
||||
html: "<p><a href=\"javascript:alert('hello!');\">link</a></p>",
|
||||
expectedResult: "<p>link</p>"
|
||||
},
|
||||
{
|
||||
html: '<p><a href="javascript:alert(document.domain);">link</a></p>',
|
||||
expectedResult: "<p>link</p>"
|
||||
},
|
||||
{
|
||||
html: '<p><a href="javascript:alert(document.domain);">link</a></p>',
|
||||
expectedResult: "<p>link</p>"
|
||||
},
|
||||
{
|
||||
html: '<p><a href="javascript&\\0colon;alert(document.domain);">link</a></p>',
|
||||
expectedResult: "<p>link</p>"
|
||||
},
|
||||
{
|
||||
html: '<p><a href="javascript&amp;\\0colon;alert(document.domain);">link</a></p>',
|
||||
expectedResult: "<p>link</p>"
|
||||
},
|
||||
{
|
||||
html: '<p><a href="https://localhost?key=123&msg=abc">link</a></p>',
|
||||
expectedResult:
|
||||
'<p><a href="https://localhost?key=123&msg=abc" rel="nofollow">link</a></p>'
|
||||
},
|
||||
{
|
||||
html: '<p><a href="https://localhost?key=abc&msg=123">link2</a></p>',
|
||||
expectedResult:
|
||||
'<p><a href="https://localhost?key=abc&msg=123" rel="nofollow">link2</a></p>'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
description: "should handle ordinary texts correctly",
|
||||
cases: [
|
||||
{
|
||||
html: "Some text",
|
||||
expectedResult: "Some text"
|
||||
},
|
||||
{
|
||||
html: `text with "double quotation"`,
|
||||
expectedResult: `text with "double quotation"`
|
||||
},
|
||||
{
|
||||
html: `text with 'single quotation'`,
|
||||
expectedResult: `text with 'single quotation'`
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
description: "should handle text styles correctly",
|
||||
cases: [
|
||||
{
|
||||
html: "<p><strong>text</strong></p>",
|
||||
expectedResult: "<p><strong>text</strong></p>"
|
||||
},
|
||||
{
|
||||
html: "<p><b>text</b></p>",
|
||||
expectedResult: "<p><b>text</b></p>"
|
||||
},
|
||||
{
|
||||
html: '<p class="red"> red text </p>',
|
||||
expectedResult: '<p class="red"> red text </p>'
|
||||
},
|
||||
{
|
||||
html: '<p align="center"> <b>red text </b></p>',
|
||||
expectedResult: '<p align="center"> <b>red text </b></p>'
|
||||
},
|
||||
{
|
||||
html: '<p align="CenTer"> <b> Case-insensitive</b></p>',
|
||||
expectedResult: '<p align="CenTer"> <b> Case-insensitive</b></p>'
|
||||
},
|
||||
{
|
||||
html: '<p align="xyz"> <b>wrong value for align</b></p>',
|
||||
expectedResult: "<p> <b>wrong value for align</b></p>"
|
||||
},
|
||||
{
|
||||
html: '<p align="centercenter"> <b>wrong value for align</b></p>',
|
||||
expectedResult: "<p> <b>wrong value for align</b></p>"
|
||||
},
|
||||
{
|
||||
html: '<p style="font-size: 20px;">This is a paragraph with larger text.</p>',
|
||||
expectedResult:
|
||||
'<p style="font-size: 20px;">This is a paragraph with larger text.</p>'
|
||||
},
|
||||
{
|
||||
html: "<h3> או נושא שתבחר</h3>",
|
||||
expectedResult: "<h3> או נושא שתבחר</h3>"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
description: "should handle styles correctly",
|
||||
cases: [
|
||||
{
|
||||
html: '<table border="5"> </table>',
|
||||
expectedResult: '<table border="5"> </table>'
|
||||
},
|
||||
{
|
||||
html: '<table border="xyz"> </table>',
|
||||
expectedResult: "<table> </table>"
|
||||
},
|
||||
{
|
||||
html: '<font color="red"> Content </font>',
|
||||
expectedResult: '<font color="red"> Content </font>'
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
const assertResult = async (
|
||||
expectedResult: string | null,
|
||||
html: string | null
|
||||
): Promise<void> => {
|
||||
if (html === null) {
|
||||
await expect(KcSanitizer.sanitize(html)).rejects.toThrow(
|
||||
"Cannot escape null value."
|
||||
);
|
||||
} else {
|
||||
const result = await KcSanitizer.sanitize(html);
|
||||
expect(result).toBe(expectedResult);
|
||||
}
|
||||
};
|
||||
|
||||
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 ordinary texts correctly", () => {
|
||||
let html: string = "";
|
||||
|
||||
html = "Some text";
|
||||
assertResult("Some text", html);
|
||||
|
||||
html = `text with "double quotation"`;
|
||||
assertResult(`text with "double quotation"`, html);
|
||||
|
||||
html = `text with 'single quotation'`;
|
||||
assertResult(`text with 'single quotation'`, 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 align="CenTer"> <b> Case-insensitive</b></p>`;
|
||||
assertResult(`<p align="CenTer"> <b> Case-insensitive</b></p>`, html);
|
||||
|
||||
html = `<p align="xyz"> <b>wrong value for align</b></p>`;
|
||||
assertResult(`<p> <b>wrong value for align</b></p>`, html);
|
||||
|
||||
html = `<p align="centercenter"> <b>wrong value for align</b></p>`;
|
||||
assertResult(`<p> <b>wrong value for align</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);
|
||||
});
|
||||
|
||||
it("should handle styles correctly", () => {
|
||||
let html = "";
|
||||
html = `<table border="5"> </table>`;
|
||||
assertResult(`<table border="5"> </table>`, html);
|
||||
|
||||
html = `<table border="xyz"> </table>`;
|
||||
assertResult(`<table> </table>`, html);
|
||||
|
||||
html = `<font color = "red"> Content </font>`;
|
||||
assertResult(`<font color="red"> Content </font>`, 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);
|
||||
expect(result).toBe(expectedResult);
|
||||
}
|
||||
// Server-side tests
|
||||
describe("KcSanitizer - Server Side", () => {
|
||||
for (const group of testCases) {
|
||||
describe(group.description, () => {
|
||||
for (const test of group.cases) {
|
||||
it(`should handle ${test.html}`, async () => {
|
||||
await assertResult(test.expectedResult, test.html);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Client-side tests
|
||||
describe("KcSanitizer - Client Side (jsdom)", () => {
|
||||
const decodeHtmlEntities = (html: string): string => {
|
||||
const entitiesMap: { [key: string]: string } = {
|
||||
"&": "&",
|
||||
"<": "<",
|
||||
">": ">",
|
||||
""": '"',
|
||||
"'": "'"
|
||||
};
|
||||
|
||||
return html.replace(
|
||||
/&|<|>|"|'/g,
|
||||
entity => entitiesMap[entity] || entity
|
||||
);
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
// Mocking the `document.createElement` to simulate textarea behavior
|
||||
vi.stubGlobal("document", {
|
||||
createElement: (tagName: string) => {
|
||||
if (tagName === "textarea") {
|
||||
let _innerHTML = "";
|
||||
return {
|
||||
get innerHTML() {
|
||||
return _innerHTML;
|
||||
},
|
||||
set innerHTML(html) {
|
||||
_innerHTML = html;
|
||||
this.value = decodeHtmlEntities(html); // Simulate decoding
|
||||
},
|
||||
value: "" // Mimic the textarea behavior where innerHTML -> value
|
||||
};
|
||||
}
|
||||
throw new Error("Unsupported element");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
for (const group of testCases) {
|
||||
describe(group.description, () => {
|
||||
for (const test of group.cases) {
|
||||
it(`should handle ${test.html}`, async () => {
|
||||
await assertResult(test.expectedResult, test.html);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user