Reorganize kcSanitarize

This commit is contained in:
Joseph Garrone
2024-09-22 18:56:05 +02:00
parent 7c553ee10d
commit b6e9043d91
8 changed files with 366 additions and 242 deletions

View File

@ -1,227 +1,137 @@
import { describe, it, expect, vi, beforeAll } from "vitest";
import { KcSanitizer } from "keycloakify/tools/kcSanitize/KcSanitizer";
import { describe, it, expect } from "vitest";
import { KcSanitizer } from "keycloakify/lib/kcSanitize/KcSanitizer";
import { decode } from "html-entities";
// 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
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&colon;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;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 = (expectedResult: string | null, html: string | null): void => {
if (html === null) {
expect(KcSanitizer.sanitize(html)).toThrow("Cannot escape null value.");
} else {
const result = KcSanitizer.sanitize(html);
expect(result).toBe(expectedResult);
}
};
describe("KeycloakSanitizerMethod", () => {
it("should handle escapes correctly", () => {
let html: string = "";
let expectedResult: string;
// 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);
// });
// }
// });
// }
// });
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);
describe("KcSanitizer - Client Side", () => {
const decodeHtmlEntities = (html: string): string => {
const entitiesMap: { [key: string]: string } = {
"&amp;": "&",
"&lt;": "<",
"&gt;": ">",
"&quot;": '"',
"&#039;": "'"
};
html = "<h1>Foo</h1>";
expectedResult = "<h1>Foo</h1>";
assertResult(expectedResult, html);
return html.replace(
/&amp;|&lt;|&gt;|&quot;|&#039;/g,
entity => entitiesMap[entity] || entity
);
};
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);
beforeAll(() => {
vi.stubGlobal("window", {});
// 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");
}
});
html = "";
expectedResult = "";
assertResult(expectedResult, html);
});
for (const group of testCases) {
describe(group.description, () => {
for (const test of group.cases) {
it(`should handle ${test.html}`, () => {
if (test.html == null)
expect(() =>
assertResult(test.expectedResult, test.html)
).toThrow("Cannot escape null value.");
else assertResult(test.expectedResult, test.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&colon;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;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;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, html: string): void {
const result = KcSanitizer.sanitize(html, decode);
expect(result).toBe(expectedResult);
}
});