mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
[IMP] runtime/utils: export htmlEscape and add tests
markup tag function requires markup awareness to determine whether a given parameter should be escaped or not. This implies that pre-escaped content should be properly marked'ed up to avoid double escaping. Having to manually wrap all calls to escape with markup is cumbersome and prone to issues (on top of having to be validated by the security team for no reason). This commit introduces a markup-aware escape function to resolve those issues.
This commit is contained in:
committed by
Géry Debongnie
parent
9d378b0e7b
commit
e788e361c7
@@ -41,7 +41,7 @@ export { useComponent, useState } from "./component_node";
|
||||
export { status } from "./status";
|
||||
export { reactive, markRaw, toRaw } from "./reactivity";
|
||||
export { useEffect, useEnv, useExternalListener, useRef, useChildSubEnv, useSubEnv } from "./hooks";
|
||||
export { batched, EventBus, whenReady, loadFile, markup } from "./utils";
|
||||
export { batched, EventBus, htmlEscape, whenReady, loadFile, markup } from "./utils";
|
||||
export {
|
||||
onWillStart,
|
||||
onMounted,
|
||||
|
||||
@@ -111,15 +111,15 @@ export async function loadFile(url: string): Promise<string> {
|
||||
*/
|
||||
export class Markup extends String {}
|
||||
|
||||
function _escapeHtml(str: any): string | Markup {
|
||||
export function htmlEscape(str: any): Markup {
|
||||
if (str instanceof Markup) {
|
||||
return str;
|
||||
}
|
||||
if (str === undefined) {
|
||||
return "";
|
||||
return markup("");
|
||||
}
|
||||
if (typeof str === "number") {
|
||||
return String(str);
|
||||
return markup(String(str));
|
||||
}
|
||||
[
|
||||
["&", "&"],
|
||||
@@ -131,7 +131,7 @@ function _escapeHtml(str: any): string | Markup {
|
||||
].forEach((pairs) => {
|
||||
str = String(str).replace(new RegExp(pairs[0], "g"), pairs[1]);
|
||||
});
|
||||
return str;
|
||||
return markup(str);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -153,7 +153,7 @@ export function markup(
|
||||
let acc = "";
|
||||
let i = 0;
|
||||
for (; i < placeholders.length; ++i) {
|
||||
acc += strings[i] + _escapeHtml(placeholders[i]);
|
||||
acc += strings[i] + htmlEscape(placeholders[i]);
|
||||
}
|
||||
acc += strings[i];
|
||||
return new Markup(acc);
|
||||
|
||||
+62
-1
@@ -1,4 +1,4 @@
|
||||
import { batched, EventBus, markup } from "../src/runtime/utils";
|
||||
import { batched, EventBus, htmlEscape, markup } from "../src/runtime/utils";
|
||||
import { nextMicroTick } from "./helpers";
|
||||
|
||||
describe("event bus behaviour", () => {
|
||||
@@ -78,6 +78,61 @@ describe("markup", () => {
|
||||
const html = markup("<blink>Hello</blink>");
|
||||
expect(html).toBeInstanceOf(Markup);
|
||||
});
|
||||
describe("htmlEscape", () => {
|
||||
test("htmlEscape escapes text", () => {
|
||||
const res = htmlEscape("<p>test</p>");
|
||||
expect(res.toString()).toBe("<p>test</p>");
|
||||
expect(res).toBeInstanceOf(Markup);
|
||||
});
|
||||
test("htmlEscape keeps html markup", () => {
|
||||
const res = htmlEscape(markup("<p>test</p>"));
|
||||
expect(res.toString()).toBe("<p>test</p>");
|
||||
expect(res).toBeInstanceOf(Markup);
|
||||
});
|
||||
test("htmlEscape produces empty string on undefined", () => {
|
||||
const res = htmlEscape(undefined);
|
||||
expect(res.toString()).toBe("");
|
||||
expect(res).toBeInstanceOf(Markup);
|
||||
});
|
||||
test("htmlEscape produces string from number", () => {
|
||||
const res = htmlEscape(10);
|
||||
expect(res.toString()).toBe("10");
|
||||
expect(res).toBeInstanceOf(Markup);
|
||||
});
|
||||
test("htmlEscape produces string from boolean", () => {
|
||||
const res = htmlEscape(false);
|
||||
expect(res.toString()).toBe("false");
|
||||
expect(res).toBeInstanceOf(Markup);
|
||||
});
|
||||
test("htmlEscape correctly escapes various links", () => {
|
||||
expect(htmlEscape("<a>this is a link</a>").toString()).toBe(
|
||||
"<a>this is a link</a>"
|
||||
);
|
||||
expect(htmlEscape(`<a href="https://www.odoo.com">odoo<a>`).toString()).toBe(
|
||||
`<a href="https://www.odoo.com">odoo<a>`
|
||||
);
|
||||
expect(htmlEscape(`<a href='https://www.odoo.com'>odoo<a>`).toString()).toBe(
|
||||
`<a href='https://www.odoo.com'>odoo<a>`
|
||||
);
|
||||
expect(htmlEscape("<a href='https://www.odoo.com'>Odoo`s website<a>").toString()).toBe(
|
||||
`<a href='https://www.odoo.com'>Odoo`s website<a>`
|
||||
);
|
||||
});
|
||||
test("htmlEscape doesn't escape already escaped content", () => {
|
||||
const res = htmlEscape("<p>test</p>");
|
||||
expect(res.toString()).toBe("<p>test</p>");
|
||||
expect(res).toBeInstanceOf(Markup);
|
||||
const res2 = htmlEscape(res);
|
||||
expect(res2.toString()).toBe("<p>test</p>");
|
||||
expect(res2).toBeInstanceOf(Markup);
|
||||
expect(res2).toBe(res);
|
||||
});
|
||||
test("htmlEscape returns markup even for only-safe text", () => {
|
||||
const res = htmlEscape("safe");
|
||||
expect(res.toString()).toBe("safe");
|
||||
expect(res).toBeInstanceOf(Markup);
|
||||
});
|
||||
});
|
||||
describe("tag function", () => {
|
||||
test("interpolated values are escaped", () => {
|
||||
const maliciousInput = "<script>alert('💥💥')</script>";
|
||||
@@ -99,5 +154,11 @@ describe("markup", () => {
|
||||
const html = markup`<img src="${imgUrl}">`;
|
||||
expect(html.toString()).toBe(`<img src="lol" onerror="alert('xss')">`);
|
||||
});
|
||||
test("already escaped content is not escaped again", () => {
|
||||
const res = htmlEscape("<p>test</p>");
|
||||
expect(res.toString()).toBe("<p>test</p>");
|
||||
const html = markup`${res}`;
|
||||
expect(html.toString()).toBe("<p>test</p>");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user