Thanks to visit codestin.com
Credit goes to github.com

Skip to content

add csp trusted types policy #102

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 21, 2023
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
59 changes: 59 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,65 @@ completer.addEventListener('auto-complete-change', function(event) {
})
```

### CSP Trusted Types

You can call
`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 `fetch` response before it is
inserted into the page:

```ts
import AutoCompleteElement from 'auto-complete-element'
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
})
}
})
AutoCompleteElement.setCSPTrustedTypesPolicy(policy)
```

The policy has access to the `fetch` response object. Due to platform
constraints, only synchronous information from the response (in addition to the
HTML text body) can be used in the policy:

```ts
import AutoCompleteElement from 'auto-complete-element'

const policy = trustedTypes.createPolicy('require-server-header', {
createHTML: (htmlText: string, response: Response) => {
if (response.headers.get('X-Server-Sanitized') !== 'sanitized=true') {
// Note: this will reject the contents, but the error may be caught before it shows in the JS console.
throw new Error('Rejecting HTML that was not marked by the server as sanitized.')
}
return htmlText
}
})
AutoCompleteElement.setCSPTrustedTypesPolicy(policy)
```

Note that:

- Only a single policy can be set, shared by all `AutoCompleteElement` fetches.
- You should call `setCSPTrustedTypesPolicy()` ahead of any other load of
`auto-complete` element in your code.
- If your policy itself requires asynchronous work to construct, you can also
pass a `Promise<TrustedTypePolicy>`.
- Pass `null` to remove the policy.
- 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.

## Browser support

Browsers without native [custom element support][support] require a [polyfill][].
Expand Down
41 changes: 38 additions & 3 deletions src/auto-complete-element.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,27 @@
import Autocomplete from './autocomplete.js'
import AutocompleteEvent from './auto-complete-event.js'
import {fragment} from './send.js'

const state = new WeakMap()

export interface CSPTrustedTypesPolicy {
createHTML: (s: string, response: Response) => CSPTrustedHTMLToStringable
}

// Note: basically every object (and some primitives) in JS satisfy this
// `CSPTrustedHTMLToStringable` interface, but this is the most compatible shape
// we can use.
interface CSPTrustedHTMLToStringable {
toString: () => string
}

let cspTrustedTypesPolicyPromise: Promise<CSPTrustedTypesPolicy> | null = null

// eslint-disable-next-line custom-elements/file-name-matches-element
export default class AutocompleteElement extends HTMLElement {
static setCSPTrustedTypesPolicy(policy: CSPTrustedTypesPolicy | Promise<CSPTrustedTypesPolicy> | null): void {
cspTrustedTypesPolicyPromise = policy === null ? policy : Promise.resolve(policy)
}

#forElement: HTMLElement | null = null
get forElement(): HTMLElement | null {
if (this.#forElement?.isConnected) {
Expand Down Expand Up @@ -87,6 +103,7 @@ export default class AutocompleteElement extends HTMLElement {
}
}

// HEAD
get fetchOnEmpty(): boolean {
return this.hasAttribute('fetch-on-empty')
}
Expand All @@ -95,8 +112,26 @@ export default class AutocompleteElement extends HTMLElement {
this.toggleAttribute('fetch-on-empty', fetchOnEmpty)
}

fetchResult = fragment

#requestController?: AbortController
async fetchResult(url: URL): Promise<string | CSPTrustedHTMLToStringable> {
this.#requestController?.abort()
const {signal} = (this.#requestController = new AbortController())
const res = await fetch(url.toString(), {
signal,
headers: {
Accept: 'text/fragment+html'
}
})
if (!res.ok) {
throw new Error(await res.text())
}
if (cspTrustedTypesPolicyPromise) {
const cspTrustedTypesPolicy = await cspTrustedTypesPolicyPromise
return cspTrustedTypesPolicy.createHTML(await res.text(), res)
}
return await res.text()
}
//f21528e (add csp trusted types policy)
static get observedAttributes(): string[] {
return ['open', 'value', 'for']
}
Expand Down
4 changes: 2 additions & 2 deletions src/autocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,10 +202,10 @@ export default class Autocomplete {

this.container.dispatchEvent(new CustomEvent('loadstart'))
this.container
.fetchResult(this.input, url.toString())
.fetchResult(url)
.then(html => {
// eslint-disable-next-line github/no-inner-html
this.results.innerHTML = html
this.results.innerHTML = html as string
this.identifyOptions()
const allNewOptions = this.results.querySelectorAll('[role="option"]')
const hasResults = !!allNewOptions.length
Expand Down
33 changes: 0 additions & 33 deletions src/send.ts

This file was deleted.

35 changes: 35 additions & 0 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,41 @@ describe('auto-complete element', function () {
assert.equal(5, list.children.length)
})
})

describe('trustedHTML', () => {
beforeEach(function () {
document.body.innerHTML = `
<div id="mocha-fixture">
<auto-complete src="/search" for="popup" data-autoselect="true">
<input type="text">
<ul id="popup"></ul>
<div id="popup-feedback"></div>
</auto-complete>
</div>
`
})

it('calls trusted types policy, passing response to it', async function () {
const calls = []
const html = '<li><strong>replacement</strong></li>'
window.AutocompleteElement.setCSPTrustedTypesPolicy({
createHTML(str, res) {
calls.push([str, res])
return html
}
})
const container = document.querySelector('auto-complete')
const popup = container.querySelector('#popup')
const input = container.querySelector('input')

triggerInput(input, 'hub')
await once(container, 'loadend')

assert.equal(calls.length, 1)
assert.equal(popup.children.length, 1)
assert.equal(popup.innerHTML, html)
})
})
})

function waitForElementToChange(el) {
Expand Down