Skip to content
This repository was archived by the owner on Jan 9, 2025. It is now read-only.

Commit aa567df

Browse files
authored
Merge pull request #26 from rzhade3/main
Add Trusted Types
2 parents 2208fa1 + 186f094 commit aa567df

8 files changed

+112
-5
lines changed

.eslintrc.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
"no-invalid-this": "off",
88
"@typescript-eslint/no-invalid-this": ["error"],
99
"import/extensions": ["error", "always"],
10-
"github/no-inner-html": "off"
10+
"github/no-inner-html": "off",
11+
"@typescript-eslint/no-unused-vars": ["error", { "vars": "all", "argsIgnorePattern": "^_" }]
1112
},
1213
"overrides": [
1314
{

README.md

+27
Original file line numberDiff line numberDiff line change
@@ -245,3 +245,30 @@ render(html`<div>${until(request, timeout, loading)}</div>`)
245245
// ^ renders <div>Loading...</div>
246246
// After 2000ms will render <div>Failed to load</div>
247247
```
248+
249+
### CSP Trusted Types
250+
251+
You can call `TemplateResult.setCSPTrustedTypesPolicy(policy: TrustedTypePolicy | Promise<TrustedTypePolicy> | null)` from JavaScript to set a [CSP trusted types policy](https://web.dev/trusted-types/), which can perform (synchronous) filtering or rejection of the rendered template:
252+
253+
```ts
254+
import {TemplateResult} from "@github/jtml";
255+
import DOMPurify from "dompurify"; // Using https://github.com/cure53/DOMPurify
256+
257+
// This policy removes all HTML markup except links.
258+
const policy = trustedTypes.createPolicy("links-only", {
259+
createHTML: (htmlText: string) => {
260+
return DOMPurify.sanitize(htmlText, {
261+
ALLOWED_TAGS: ["a"],
262+
ALLOWED_ATTR: ["href"],
263+
RETURN_TRUSTED_TYPE: true,
264+
});
265+
},
266+
});
267+
TemplateResult.setCSPTrustedTypesPolicy(policy);
268+
```
269+
270+
Note that:
271+
272+
- Only a single policy can be set, shared by all `render` and `unsafeHTML` calls.
273+
- You should call `TemplateResult.setCSPTrustedTypesPolicy()` ahead of any other call of `@github/jtml` in your code.
274+
- Not all browsers [support the trusted types API in JavaScript](https://caniuse.com/mdn-api_trustedtypes). You may want to use the [recommended tinyfill](https://github.com/w3c/trusted-types#tinyfill) to construct a policy without causing issues in other browsers.

src/template-result.ts

+18-1
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,37 @@ import type {TemplateTypeInit} from '@github/template-parts'
44
const templates = new WeakMap<TemplateStringsArray, HTMLTemplateElement>()
55
const renderedTemplates = new WeakMap<Node | NodeTemplatePart, HTMLTemplateElement>()
66
const renderedTemplateInstances = new WeakMap<Node | NodeTemplatePart, TemplateInstance>()
7+
8+
interface CSPTrustedHTMLToStringable {
9+
toString: () => string
10+
}
11+
12+
interface CSPTrustedTypesPolicy {
13+
createHTML: (s: string) => CSPTrustedHTMLToStringable
14+
}
15+
716
export class TemplateResult {
817
constructor(
918
public readonly strings: TemplateStringsArray,
1019
public readonly values: unknown[],
1120
public processor: TemplateTypeInit
1221
) {}
1322

23+
static cspTrustedTypesPolicy: CSPTrustedTypesPolicy | null = null
24+
25+
static setCSPTrustedTypesPolicy(policy: CSPTrustedTypesPolicy | null) {
26+
TemplateResult.cspTrustedTypesPolicy = policy
27+
}
28+
1429
get template(): HTMLTemplateElement {
1530
if (templates.has(this.strings)) {
1631
return templates.get(this.strings)!
1732
} else {
1833
const template = document.createElement('template')
1934
const end = this.strings.length - 1
20-
template.innerHTML = this.strings.reduce((str, cur, i) => str + cur + (i < end ? `{{ ${i} }}` : ''), '')
35+
const html = this.strings.reduce((str, cur, i) => str + cur + (i < end ? `{{ ${i} }}` : ''), '')
36+
const trustedHtml = (TemplateResult.cspTrustedTypesPolicy?.createHTML(html) as string | undefined) ?? html
37+
template.innerHTML = trustedHtml
2138
templates.set(this.strings, template)
2239
return template
2340
}

src/trusted-types.ts

Whitespace-only changes.

src/unsafe-html.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import {directive} from './directive.js'
22
import {NodeTemplatePart} from '@github/template-parts'
33
import type {TemplatePart} from '@github/template-parts'
4+
import {TemplateResult} from './template-result.js'
45

56
export const unsafeHTML = directive((value: string) => (part: TemplatePart) => {
67
if (!(part instanceof NodeTemplatePart)) return
78
const template = document.createElement('template')
8-
template.innerHTML = value
9+
const trustedValue = (TemplateResult.cspTrustedTypesPolicy?.createHTML(value) as string | undefined) ?? value
10+
template.innerHTML = trustedValue
911
const fragment = document.importNode(template.content, true)
1012
part.replace(...fragment.childNodes)
1113
})

test/render.ts

+22-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {expect} from 'chai'
2-
import {html, render} from '../lib/index.js'
2+
import {html, render, TemplateResult} from '../lib/index.js'
33
import type {TemplateResult} from '../lib/index.js'
44

55
describe('render', () => {
@@ -8,6 +8,10 @@ describe('render', () => {
88
surface = document.createElement('section')
99
})
1010

11+
afterEach(() => {
12+
TemplateResult.setCSPTrustedTypesPolicy(null)
13+
})
14+
1115
it('memoizes by TemplateResult#template, updating old templates with new values', () => {
1216
const main = (x: string | null = null) => html`<div class="${x}"></div>`
1317
render(main('foo'), surface)
@@ -55,4 +59,21 @@ describe('render', () => {
5559
expect(surface.innerHTML).to.contain('<div><span><div></div></span><span><div></div></span></div>')
5660
})
5761
})
62+
63+
describe('trusted types', () => {
64+
it('respects a Trusted Types Policy if it is set', () => {
65+
let policyCalled = false
66+
const rewrittenFragment = '<div id="bar"></div>'
67+
TemplateResult.setCSPTrustedTypesPolicy({
68+
createHTML: (_html: string) => {
69+
policyCalled = true
70+
return rewrittenFragment
71+
}
72+
})
73+
const main = (x: string | null = null) => html`<div class="${x}"></div>`
74+
render(main('foo'), surface)
75+
expect(surface.innerHTML).to.equal(rewrittenFragment)
76+
expect(policyCalled).to.be.true
77+
})
78+
})
5879
})

test/trusted-types.ts

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import {expect} from 'chai'
2+
import {TemplateResult} from '../lib/index.js'
3+
4+
describe('trusted types', () => {
5+
after(() => {
6+
TemplateResult.setCSPTrustedTypesPolicy(null)
7+
})
8+
9+
it('can set a CSP Trusted Types policy', () => {
10+
const dummyPolicy = {
11+
createHTML: (htmlText: string) => {
12+
return htmlText
13+
}
14+
}
15+
expect(TemplateResult.cspTrustedTypesPolicy).to.equal(null)
16+
TemplateResult.setCSPTrustedTypesPolicy(dummyPolicy)
17+
expect(TemplateResult.cspTrustedTypesPolicy).to.equal(dummyPolicy)
18+
})
19+
})

test/unsafe-html.ts

+21-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import {expect} from 'chai'
2-
import {html, render, unsafeHTML} from '../lib/index.js'
2+
import {html, render, TemplateResult, unsafeHTML} from '../lib/index.js'
33

44
describe('unsafeHTML', () => {
5+
beforeEach(() => {
6+
TemplateResult.setCSPTrustedTypesPolicy(null)
7+
})
8+
afterEach(() => {
9+
TemplateResult.setCSPTrustedTypesPolicy(null)
10+
})
511
it('renders basic text', async () => {
612
const surface = document.createElement('section')
713
render(html`<div>${unsafeHTML('Hello World')}</div>`, surface)
@@ -31,4 +37,18 @@ describe('unsafeHTML', () => {
3137
render(fn('<a href="">Universe</a>'), surface)
3238
expect(surface.innerHTML).to.equal('<div><span>Hello</span><span><a href="">Universe</a></span></div>')
3339
})
40+
it('respects trusted types', async () => {
41+
let policyCalled = false
42+
const rewrittenFragment = '<div id="bar">This has been rewritten by Trusted Types.</div>'
43+
TemplateResult.setCSPTrustedTypesPolicy({
44+
createHTML: (_html: string) => {
45+
policyCalled = true
46+
return rewrittenFragment
47+
}
48+
})
49+
const surface = document.createElement('section')
50+
render(html`<div>${unsafeHTML('<span>Hello</span><span>World</span>')}</div>`, surface)
51+
expect(surface.innerHTML).to.equal(rewrittenFragment)
52+
expect(policyCalled).to.be.true
53+
})
3454
})

0 commit comments

Comments
 (0)