Skip to content

Commit

Permalink
Merge pull request #26 from rzhade3/main
Browse files Browse the repository at this point in the history
Add Trusted Types
  • Loading branch information
rzhade3 authored Dec 9, 2022
2 parents 2208fa1 + 186f094 commit aa567df
Show file tree
Hide file tree
Showing 8 changed files with 112 additions and 5 deletions.
3 changes: 2 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"no-invalid-this": "off",
"@typescript-eslint/no-invalid-this": ["error"],
"import/extensions": ["error", "always"],
"github/no-inner-html": "off"
"github/no-inner-html": "off",
"@typescript-eslint/no-unused-vars": ["error", { "vars": "all", "argsIgnorePattern": "^_" }]
},
"overrides": [
{
Expand Down
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,3 +245,30 @@ render(html`<div>${until(request, timeout, loading)}</div>`)
// ^ renders <div>Loading...</div>
// After 2000ms will render <div>Failed to load</div>
```
### CSP Trusted Types
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:
```ts
import {TemplateResult} from "@github/jtml";
import DOMPurify from "dompurify"; // Using https://github.com/cure53/DOMPurify
// This policy removes all HTML markup except links.
const policy = trustedTypes.createPolicy("links-only", {
createHTML: (htmlText: string) => {
return DOMPurify.sanitize(htmlText, {
ALLOWED_TAGS: ["a"],
ALLOWED_ATTR: ["href"],
RETURN_TRUSTED_TYPE: true,
});
},
});
TemplateResult.setCSPTrustedTypesPolicy(policy);
```
Note that:
- Only a single policy can be set, shared by all `render` and `unsafeHTML` calls.
- You should call `TemplateResult.setCSPTrustedTypesPolicy()` ahead of any other call of `@github/jtml` in your code.
- 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.
19 changes: 18 additions & 1 deletion src/template-result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,37 @@ import type {TemplateTypeInit} from '@github/template-parts'
const templates = new WeakMap<TemplateStringsArray, HTMLTemplateElement>()
const renderedTemplates = new WeakMap<Node | NodeTemplatePart, HTMLTemplateElement>()
const renderedTemplateInstances = new WeakMap<Node | NodeTemplatePart, TemplateInstance>()

interface CSPTrustedHTMLToStringable {
toString: () => string
}

interface CSPTrustedTypesPolicy {
createHTML: (s: string) => CSPTrustedHTMLToStringable
}

export class TemplateResult {
constructor(
public readonly strings: TemplateStringsArray,
public readonly values: unknown[],
public processor: TemplateTypeInit
) {}

static cspTrustedTypesPolicy: CSPTrustedTypesPolicy | null = null

static setCSPTrustedTypesPolicy(policy: CSPTrustedTypesPolicy | null) {
TemplateResult.cspTrustedTypesPolicy = policy
}

get template(): HTMLTemplateElement {
if (templates.has(this.strings)) {
return templates.get(this.strings)!
} else {
const template = document.createElement('template')
const end = this.strings.length - 1
template.innerHTML = this.strings.reduce((str, cur, i) => str + cur + (i < end ? `{{ ${i} }}` : ''), '')
const html = this.strings.reduce((str, cur, i) => str + cur + (i < end ? `{{ ${i} }}` : ''), '')
const trustedHtml = (TemplateResult.cspTrustedTypesPolicy?.createHTML(html) as string | undefined) ?? html
template.innerHTML = trustedHtml
templates.set(this.strings, template)
return template
}
Expand Down
Empty file added src/trusted-types.ts
Empty file.
4 changes: 3 additions & 1 deletion src/unsafe-html.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import {directive} from './directive.js'
import {NodeTemplatePart} from '@github/template-parts'
import type {TemplatePart} from '@github/template-parts'
import {TemplateResult} from './template-result.js'

export const unsafeHTML = directive((value: string) => (part: TemplatePart) => {
if (!(part instanceof NodeTemplatePart)) return
const template = document.createElement('template')
template.innerHTML = value
const trustedValue = (TemplateResult.cspTrustedTypesPolicy?.createHTML(value) as string | undefined) ?? value
template.innerHTML = trustedValue
const fragment = document.importNode(template.content, true)
part.replace(...fragment.childNodes)
})
23 changes: 22 additions & 1 deletion test/render.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {expect} from 'chai'
import {html, render} from '../lib/index.js'
import {html, render, TemplateResult} from '../lib/index.js'
import type {TemplateResult} from '../lib/index.js'

describe('render', () => {
Expand All @@ -8,6 +8,10 @@ describe('render', () => {
surface = document.createElement('section')
})

afterEach(() => {
TemplateResult.setCSPTrustedTypesPolicy(null)
})

it('memoizes by TemplateResult#template, updating old templates with new values', () => {
const main = (x: string | null = null) => html`<div class="${x}"></div>`
render(main('foo'), surface)
Expand Down Expand Up @@ -55,4 +59,21 @@ describe('render', () => {
expect(surface.innerHTML).to.contain('<div><span><div></div></span><span><div></div></span></div>')
})
})

describe('trusted types', () => {
it('respects a Trusted Types Policy if it is set', () => {
let policyCalled = false
const rewrittenFragment = '<div id="bar"></div>'
TemplateResult.setCSPTrustedTypesPolicy({
createHTML: (_html: string) => {
policyCalled = true
return rewrittenFragment
}
})
const main = (x: string | null = null) => html`<div class="${x}"></div>`
render(main('foo'), surface)
expect(surface.innerHTML).to.equal(rewrittenFragment)
expect(policyCalled).to.be.true
})
})
})
19 changes: 19 additions & 0 deletions test/trusted-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {expect} from 'chai'
import {TemplateResult} from '../lib/index.js'

describe('trusted types', () => {
after(() => {
TemplateResult.setCSPTrustedTypesPolicy(null)
})

it('can set a CSP Trusted Types policy', () => {
const dummyPolicy = {
createHTML: (htmlText: string) => {
return htmlText
}
}
expect(TemplateResult.cspTrustedTypesPolicy).to.equal(null)
TemplateResult.setCSPTrustedTypesPolicy(dummyPolicy)
expect(TemplateResult.cspTrustedTypesPolicy).to.equal(dummyPolicy)
})
})
22 changes: 21 additions & 1 deletion test/unsafe-html.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import {expect} from 'chai'
import {html, render, unsafeHTML} from '../lib/index.js'
import {html, render, TemplateResult, unsafeHTML} from '../lib/index.js'

describe('unsafeHTML', () => {
beforeEach(() => {
TemplateResult.setCSPTrustedTypesPolicy(null)
})
afterEach(() => {
TemplateResult.setCSPTrustedTypesPolicy(null)
})
it('renders basic text', async () => {
const surface = document.createElement('section')
render(html`<div>${unsafeHTML('Hello World')}</div>`, surface)
Expand Down Expand Up @@ -31,4 +37,18 @@ describe('unsafeHTML', () => {
render(fn('<a href="">Universe</a>'), surface)
expect(surface.innerHTML).to.equal('<div><span>Hello</span><span><a href="">Universe</a></span></div>')
})
it('respects trusted types', async () => {
let policyCalled = false
const rewrittenFragment = '<div id="bar">This has been rewritten by Trusted Types.</div>'
TemplateResult.setCSPTrustedTypesPolicy({
createHTML: (_html: string) => {
policyCalled = true
return rewrittenFragment
}
})
const surface = document.createElement('section')
render(html`<div>${unsafeHTML('<span>Hello</span><span>World</span>')}</div>`, surface)
expect(surface.innerHTML).to.equal(rewrittenFragment)
expect(policyCalled).to.be.true
})
})

0 comments on commit aa567df

Please sign in to comment.