Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions doc/reference/templates.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,15 @@ The first `t-out` will act as a `t-esc` directive, which means that the content
of `value1` will be escaped. However, since `value2` has been tagged as a markup,
this will be injected as html.

`markup` can also be used as a tag function, allowing the interpolated values to
be safely escaped:

```js
const maliciousInput = "<script>alert('💥💥')</script>";
// <b>&lt;script&gt;alert(&#x27;💥💥&#x27;)&lt;/script&gt;</b>
const value = markup`<b>${maliciousInput}</b>`;
```

### Setting Variables

QWeb allows creating variables from within the template, to memoize a computation (to use it multiple times), give a piece of data a clearer name, ...
Expand Down
44 changes: 42 additions & 2 deletions src/runtime/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,50 @@ export async function loadFile(url: string): Promise<string> {
*/
export class Markup extends String {}

function _escapeHtml(str: any): string | Markup {
if (str instanceof Markup) {
return str;
}
if (str === undefined) {
return "";
}
if (typeof str === "number") {
return String(str);
}
[
["&", "&amp;"],
["<", "&lt;"],
[">", "&gt;"],
["'", "&#x27;"],
['"', "&quot;"],
["`", "&#x60;"],
].forEach((pairs) => {
str = String(str).replace(new RegExp(pairs[0], "g"), pairs[1]);
});
return str;
}

/*
* Marks a value as safe, that is, a value that can be injected as HTML directly.
* It should be used to wrap the value passed to a t-out directive to allow a raw rendering.
*
* If called as a tag function, the interpolated strings are escaped.
*/
export function markup(value: any) {
return new Markup(value);
export function markup(strings: TemplateStringsArray, ...placeholders: unknown[]): Markup;
export function markup(value: string): Markup;
export function markup(
valueOrStrings: string | TemplateStringsArray,
...placeholders: unknown[]
): Markup {
if (!Array.isArray(valueOrStrings)) {
return new Markup(valueOrStrings);
}
const strings = valueOrStrings;
let acc = "";
let i = 0;
for (; i < placeholders.length; ++i) {
acc += strings[i] + _escapeHtml(placeholders[i]);
}
acc += strings[i];
return new Markup(acc);
}
32 changes: 31 additions & 1 deletion tests/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { batched, EventBus } from "../src/runtime/utils";
import { batched, EventBus, markup } from "../src/runtime/utils";
import { nextMicroTick } from "./helpers";

describe("event bus behaviour", () => {
Expand Down Expand Up @@ -71,3 +71,33 @@ describe("batched", () => {
expect(n).toBe(2);
});
});

const Markup = markup("").constructor;
describe("markup", () => {
test("string is flagged as safe", () => {
const html = markup("<blink>Hello</blink>");
expect(html).toBeInstanceOf(Markup);
});
describe("tag function", () => {
test("interpolated values are escaped", () => {
const maliciousInput = "<script>alert('💥💥')</script>";
const html = markup`<b>${maliciousInput}</b>`;
expect(html.toString()).toBe("<b>&lt;script&gt;alert(&#x27;💥💥&#x27;)&lt;/script&gt;</b>");
expect(html).toBeInstanceOf(Markup);
});
test("interpolated markups aren't escaped", () => {
const shouldBeEscaped = "<script>alert('should be escaped')</script>";
const shouldnt = markup("<b>this is safe</b>");
const html = markup`<div>${shouldBeEscaped} ${shouldnt}</div>`;
expect(html.toString()).toBe(
"<div>&lt;script&gt;alert(&#x27;should be escaped&#x27;)&lt;/script&gt; <b>this is safe</b></div>"
);
expect(html).toBeInstanceOf(Markup);
});
test("quotes in interpolated values are escaped", () => {
const imgUrl = `lol" onerror="alert('xss')`;
const html = markup`<img src="${imgUrl}">`;
expect(html.toString()).toBe(`<img src="lol&quot; onerror=&quot;alert(&#x27;xss&#x27;)">`);
});
});
});