From 06df963b23b3039e5869fafe7dd4c29e6e6de6b1 Mon Sep 17 00:00:00 2001 From: Chris Zuber Date: Tue, 2 Apr 2024 18:18:44 -0700 Subject: [PATCH] Update Sanitizer & Trusted Types API polyfills ...Which mean numerous breaking changes throughout. --- .eslintrc.json | 1 - CHANGELOG.md | 8 +- dom.js | 26 ++---- elements.js | 27 ++---- http.js | 21 ++--- markdown.js | 18 ++-- package-lock.json | 4 +- package.json | 2 +- test/index.html | 59 ++++++++----- test/js/index.js | 20 +---- test/js/policy.js | 18 ++++ trust-policies.js | 221 +++++++++++----------------------------------- 12 files changed, 152 insertions(+), 273 deletions(-) create mode 100644 test/js/policy.js diff --git a/.eslintrc.json b/.eslintrc.json index 36e2955..4234745 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -31,7 +31,6 @@ "no-prototype-builtins": 0 }, "globals": { - "Sanitizer": "readonly", "AggregateError": "readonly", "cookieStore": "readonly", "globalThis": "readonly", diff --git a/CHANGELOG.md b/CHANGELOG.md index ccde5d1..120112c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [v1.0.0] - 2024-04-02 + +### Changed +- Updated to more recent polyfills for Sanitizer API & Trusted Types API +- Massive breaking changes in multiple places + ## [v0.3.4] - 2024-03-15 ### Added @@ -149,7 +155,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Add `markdown` module -- Add `isSafeHTML()` to `trust-policies` module (checks for unsafe elements & attributes) +- Add `()` to `trust-policies` module (checks for unsafe elements & attributes) - Add `getCSSStyleSheet()` to `http` module - Add Markdown to `types` module - Add `gravatar` module diff --git a/dom.js b/dom.js index fa892cc..c409c24 100644 --- a/dom.js +++ b/dom.js @@ -239,30 +239,20 @@ export function text(what, text, { base } = {}) { export function html(what, text, { base, - sanitizer, policy = 'trustedTypes' in globalThis ? globalThis.trustedTypes.defaultPolicy : null, - allowElements, - allowComments, - allowAttributes, - allowCustomElements, - blockElements, - dropAttributes, - dropElements, - allowUnknownMarkup, + sanitizer: { + elements, + attributes, + comments, + } = {} } = {}) { if ( Element.prototype.setHTML instanceof Function - && typeof allowAttributes !== 'undefined' - && typeof allowElements !== 'undefined' + && typeof attributes !== 'undefined' + && typeof elements !== 'undefined' ) { const tmp = document.createElement('div'); - tmp.setHTML({ - allowComments, allowAttributes, allowCustomElements, blockElements, - dropAttributes, dropElements, allowUnknownMarkup, - }); - return each(what, el => el.append(...tmp.cloneNode(true).children), { base }); - } else if (typeof sanitizer !== 'undefined' && Element.prototype.setHTML instanceof Function) { - const tmp = tmp.setHTML(text, { sanitizer }); + tmp.setHTML({ sanitizer: { elements, attributes, comments }}); return each(what, el => el.append(...tmp.cloneNode(true).children), { base }); } else if (isHTML(text)) { return each(what, el => el.innerHTML = text, { base }); diff --git a/elements.js b/elements.js index 0fa28c3..e7fb5b2 100644 --- a/elements.js +++ b/elements.js @@ -1,5 +1,5 @@ /** - * @copyright 2023 Chris Zuber + * @copyright 2023-2024 Chris Zuber */ import { data, attr, css, getAttrs, aria as setAria } from './attrs.js'; import { listen } from './events.js'; @@ -76,15 +76,11 @@ export function createElement(tag, { text = undefined, html = undefined, policy = 'trustedTypes' in globalThis ? globalThis.trustedTypes.defaultPolicy : null, - sanitizer = undefined, - allowElements, - allowComments, - allowAttributes, - allowCustomElements, - blockElements, - dropAttributes, - dropElements, - allowUnknownMarkup, + sanitizer: { + elements, + comments, + attributes, + } = {}, aria = undefined, events: { capture, passive, once, signal, ...events } = {}, animation: { @@ -150,16 +146,9 @@ export function createElement(tag, { } else if (typeof html === 'string' || isHTML(html)) { if ( Element.prototype.setHTML instanceof Function - && ( - (typeof allowElements !== 'undefined' && typeof allowAttributes !== 'undefined') - || ('Sanitizer' in globalThis && sanitizer instanceof globalThis.Sanitizer) - ) + && (typeof elements !== 'undefined' && typeof attributes !== 'undefined') ) { - el.setHTML(html, { - allowElements, allowComments, allowAttributes, - allowCustomElements, blockElements, dropAttributes, dropElements, - allowUnknownMarkup, sanitizer, - }); + el.setHTML(html, { sanitizer: { elements, attributes, comments }}); } else { setProp(el, 'innerHTML', html, { policy }); } diff --git a/http.js b/http.js index a32229f..3f596e6 100644 --- a/http.js +++ b/http.js @@ -1,5 +1,5 @@ /** - * @copyright 2021-2023 Chris Zuber + * @copyright 2021-2024 Chris Zuber */ import { parse } from './dom.js'; import { signalAborted } from './abort.js'; @@ -229,15 +229,11 @@ export async function getHTML(url, { keepalive = undefined, signal = undefined, timeout = null, - sanitizer = undefined, - allowElements, - allowComments, - allowAttributes, - allowCustomElements, - blockElements, - dropAttributes, - dropElements, - allowUnknownMarkup, + sanitizer: { + elements, + attributes, + comments, + } = {}, policy, errorMessage, } = {}) { @@ -251,10 +247,7 @@ export async function getHTML(url, { return frag; } else { const frag = document.createDocumentFragment(); - const doc = Document.parseHTML(html, { - sanitizer, allowElements, allowComments, allowAttributes, allowCustomElements, - blockElements, dropAttributes, dropElements, allowUnknownMarkup, - }); + const doc = Document.parseHTML(html, { sanitizer: { elements, attributes, comments }}); frag.append(...doc.head.children, ...doc.body.children); return frag; } diff --git a/markdown.js b/markdown.js index 19b5e88..a7f122d 100644 --- a/markdown.js +++ b/markdown.js @@ -40,15 +40,15 @@ use(markedHighlight({ export async function parseMarkdown(str, { base = document.baseURI, - allowElements = ALLOW_ELEMENTS, - allowAttributes = ALLOW_ATTRIBUTES, + elements = ALLOW_ELEMENTS, + attributes = ALLOW_ATTRIBUTES, } = {}) { const { resolve, reject, promise } = Promise.withResolvers(); requestIdleCallback(() => { try { const parsed = parse(str); - const doc = Document.parseHTML(parsed, { allowElements, allowAttributes }); + const doc = Document.parseHTML(parsed, { sanitizer: { elements, attributes }}); const frag = document.createDocumentFragment(); doc.querySelectorAll('img').forEach(img => { @@ -74,8 +74,8 @@ export async function parseMarkdown(str, { export async function parseMarkdownFile(file, { base = document.baseURI, - allowElements = ALLOW_ELEMENTS, - allowAttributes = ALLOW_ATTRIBUTES, + elements = ALLOW_ELEMENTS, + attributes = ALLOW_ATTRIBUTES, } = {}) { if (! (file instanceof File)) { throw new TypeError('Not a file.'); @@ -83,14 +83,14 @@ export async function parseMarkdownFile(file, { throw new TypeError(`${file.name} is not a markdown file.`); } else { const text = await file.text(); - return await parseMarkdown(text, { base, allowElements, allowAttributes }); + return await parseMarkdown(text, { base, elements, attributes }); } } export async function fetchMarkdown(url, { base = document.baseURI, - allowElements = ALLOW_ELEMENTS, - allowAttributes = ALLOW_ATTRIBUTES, + elements = ALLOW_ELEMENTS, + attributes = ALLOW_ATTRIBUTES, headers = {}, mode = 'cors', cache = 'default', @@ -111,6 +111,6 @@ export async function fetchMarkdown(url, { throw new TypeError(`Expected "Content-Type: ${MARKDOWN}" but got ${resp.headers.get('Content-Type')}.`); } else { const text = await resp.text(); - return await parseMarkdown(text, { base, allowElements, allowAttributes }); + return await parseMarkdown(text, { base, elements, attributes }); } } diff --git a/package-lock.json b/package-lock.json index 9e213b3..4165647 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@shgysk8zer0/kazoo", - "version": "0.3.4", + "version": "1.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@shgysk8zer0/kazoo", - "version": "0.3.4", + "version": "1.0.0", "funding": [ { "type": "librepay", diff --git a/package.json b/package.json index 5bf380d..ee5cdbb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@shgysk8zer0/kazoo", - "version": "0.3.4", + "version": "1.0.0", "private": false, "type": "module", "description": "A JavaScript monorepo for all the things!", diff --git a/test/index.html b/test/index.html index df25714..9f85879 100644 --- a/test/index.html +++ b/test/index.html @@ -1,5 +1,5 @@ - + @@ -22,7 +22,7 @@ connect-src 'self' https://unpkg.com/ https://api.github.com/users/ https://api.github.com/repos/ https://api.pwnedpasswords.com/range/ https://maps.kernvalley.us/places/ https://events.kernvalley.us/events.json https://events.kernvalley.us/cal/krv-events.ics https://whiskeyflatdays.com/events.json https://whiskeyflatdays.com/mayors/events.json; img-src * data: blob:; require-trusted-types-for 'script'; - trusted-types empty#html empty#script default sanitizer-raw#html trust-raw#html youtube#script-url github-user#html github-repo#html blob#script-url goog-cal#script-url goog-maps#script-url krv#embed; + trusted-types default aegis-sanitizer#html youtube#script-url github-user#html github-repo#html blob#script-url goog-cal#script-url goog-maps#script-url krv#embed; upgrade-insecure-requests;" /> Kazoo @@ -31,29 +31,48 @@ { "imports": { "@shgysk8zer0/kazoo/": "../", - "@shgysk8zer0/konami": "https://unpkg.com/@shgysk8zer0/konami@1.0.10/konami.js", - "@shgysk8zer0/polyfills": "https://unpkg.com/@shgysk8zer0/polyfills@0.3.0/all.min.js", - "@shgysk8zer0/polyfills/": "https://unpkg.com/@shgysk8zer0/polyfills@0.3.0/", + "@shgysk8zer0/konami": "https://unpkg.com/@shgysk8zer0/konami@1.1.1/konami.js", + "@shgysk8zer0/polyfills": "https://unpkg.com/@shgysk8zer0/polyfills@0.3.7/all.min.js", + "@shgysk8zer0/polyfills/": "https://unpkg.com/@shgysk8zer0/polyfills@0.3.7/", + "@shgysk8zer0/jswaggersheets": "https://unpkg.com/@shgysk8zer0/jswaggersheets@1.1.0/swagger.js", + "@shgysk8zer0/jswaggersheets/": "https://unpkg.com/@shgysk8zer0/jswaggersheets@1.1.0/", + "@shgysk8zer0/jss/": "https://unpkg.com/@shgysk8zer0/jss@1.0.1/", "@shgysk8zer0/consts/": "https://unpkg.com/@shgysk8zer0/consts@1.0.8/", - "@shgysk8zer0/jswaggersheets": "https://unpkg.com/@shgysk8zer0/jswaggersheets@1.0.4/swagger.js", - "@shgysk8zer0/jswaggersheets/": "https://unpkg.com/@shgysk8zer0/jswaggersheets@1.0.4/", - "@shgysk8zer0/http-status": "https://unpkg.com/@shgysk8zer0/http-status@1.0.2/http-status.js", - "@shgysk8zer0/components/": "https://unpkg.com/@shgysk8zer0/components@0.0.12/", - "@kernvalley/components/": "https://unpkg.com/@kernvalley/components@1.1.2/", + "@shgysk8zer0/http/": "https://unpkg.com/@shgysk8zer0/http@1.0.5/", + "@shgysk8zer0/http-status": "https://unpkg.com/@shgysk8zer0/http-status@1.1.1/http-status.js", + "@aegisjsproject/trusted-types": "https://unpkg.com/@aegisjsproject/trusted-types@1.0.1/bundle.min.js", + "@aegisjsproject/trusted-types/": "https://unpkg.com/@aegisjsproject/trusted-types@1.0.1/", + "@aegisjsproject/parsers": "https://unpkg.com/@aegisjsproject/parsers@0.0.6/bundle.min.js", + "@aegisjsproject/parsers/": "https://unpkg.com/@aegisjsproject/parsers@0.0.6/", + "@aegisjsproject/sanitizer": "https://unpkg.com/@aegisjsproject/sanitizer@0.0.9/sanitizer.js", + "@aegisjsproject/sanitizer/": "https://unpkg.com/@aegisjsproject/sanitizer@0.0.9/", + "@aegisjsproject/core": "https://unpkg.com/@aegisjsproject/core@0.2.5/core.js", + "@aegisjsproject/core/": "https://unpkg.com/@aegisjsproject/core@0.2.5/", + "@aegisjsproject/styles": "https://unpkg.com/@aegisjsproject/styles@0.1.1/styles.js", + "@aegisjsproject/styles/": "https://unpkg.com/@aegisjsproject/styles@0.1.1/", + "@aegisjsproject/component": "https://unpkg.com/@aegisjsproject/component@0.1.2/component.js", + "@aegisjsproject/component/": "https://unpkg.com/@aegisjsproject/component@0.1.2/", + "@aegisjsproject/markdown": "https://unpkg.com/@aegisjsproject/markdown@0.1.2/markdown.js", + "@aegisjsproject/markdown/": "https://unpkg.com/@aegisjsproject/markdown@0.1.2/", + "@aegisjsproject/aegis-md": "https://unpkg.com/@aegisjsproject/aegis-md@0.0.2/aegis-md.js", + "@aegisjsproject/aegis-md/": "https://unpkg.com/@aegisjsproject/aegis-md@0.0.2/", + "@shgysk8zer0/components/": "https://unpkg.com/@shgysk8zer0/components@0.1.6/", + "@kernvalley/components/": "https://unpkg.com/@kernvalley/components@1.1.5/", "@webcomponents/custom-elements": "https://unpkg.com/@webcomponents/custom-elements@1.6.0/custom-elements.min.js", "leaflet": "https://unpkg.com/leaflet@1.9.4/dist/leaflet-src.esm.js", - "firebase/": "https://www.gstatic.com/firebasejs/9.22.2/", - "urlpattern-polyfill": "https://unpkg.com/urlpattern-polyfill@9.0.0/index.js", - "highlight.js": "https://unpkg.com/@highlightjs/cdn-assets@11.8.0/es/highlight.min.js", - "highlight.js/": "https://unpkg.com/@highlightjs/cdn-assets@11.8.0/", - "marked": "https://unpkg.com/marked@5.1.0/src/marked.js", - "marked-highlight": "https://unpkg.com/marked-highlight@2.0.1/src/index.js" - }, - "scope": {} + "urlpattern-polyfill": "https://unpkg.com/urlpattern-polyfill@10.0.0/index.js", + "highlight.js": "https://unpkg.com/@highlightjs/cdn-assets@11.9.0/es/core.min.js", + "highlight.js/": "https://unpkg.com/@highlightjs/cdn-assets@11.9.0/es/", + "@highlightjs/cdn-assets": "https://unpkg.com/@highlightjs/cdn-assets@11.9.0/es/core.min.js", + "@highlightjs/cdn-assets/": "https://unpkg.com/@highlightjs/cdn-assets@11.9.0/es/", + "marked": "https://unpkg.com/marked@12.0.1/lib/marked.esm.js", + "marked-highlight": "https://unpkg.com/marked-highlight@2.1.1/src/index.js", + "firebase/": "https://www.gstatic.com/firebasejs/9.23.0/" + } } - - + + diff --git a/test/js/index.js b/test/js/index.js index 7551707..b27db7f 100644 --- a/test/js/index.js +++ b/test/js/index.js @@ -2,8 +2,6 @@ import { createElement } from '../../elements.js'; import { getJSON, getHTML } from '../../http.js'; import { html, attr, each } from '../../dom.js'; import { animate } from '../../animate.js'; -import { createPolicy } from '../../trust.js'; -import { isTrustedScriptOrigin } from '../../trust-policies.js'; import { createYouTubeEmbed } from '../../youtube.js'; import { createSVGFile } from '../../svg.js'; import * as icons from '../../icons.js'; @@ -18,22 +16,6 @@ import { addStyle } from '@shgysk8zer0/jswaggersheets'; import '@shgysk8zer0/components/github/user.js'; import '@shgysk8zer0/components/github/repo.js'; -const policy = createPolicy('default', { - createHTML: input => { - const el = document.createElement('div'); - el.setHTML(input); - return el.innerHTML; - }, - createScript: () => trustedTypes.emptyScript, - createScriptURL: input => { - if (isTrustedScriptOrigin(input)) { - return input; - } else { - throw new DOMException(`Untrusted Script URL: ${input}`); - } - } -}); - addStyle(document, btnStyles); each('[data-template]', el => @@ -56,7 +38,7 @@ document.getElementById('footer').append( ); getJSON('./api/bacon.json').then(async lines => { - html('#bacon', policy.createHTML(lines.map(t => `

${t}

`).join(''))); + html('#bacon', trustedTypes.defaultPolicy.createHTML(lines.map(t => `

${t}

`).join(''))); document.getElementById('header') .append(...Object.entries(icons).map(([ariaLabel, func]) => func({ size: 64, ariaLabel }))); diff --git a/test/js/policy.js b/test/js/policy.js new file mode 100644 index 0000000..ed3592a --- /dev/null +++ b/test/js/policy.js @@ -0,0 +1,18 @@ +import { isTrustedScriptOrigin } from '../../trust-policies.js'; +// import { createPolicy } from '../../trust.js'; + +trustedTypes.createPolicy('default', { + createHTML: input => { + const el = document.createElement('div'); + el.setHTML(input); + return el.innerHTML; + }, + createScript: () => trustedTypes.emptyScript, + createScriptURL: input => { + if (isTrustedScriptOrigin(input)) { + return input; + } else { + throw new DOMException(`Untrusted Script URL: ${input}`); + } + } +}); diff --git a/trust-policies.js b/trust-policies.js index b7cf62a..aa630d5 100644 --- a/trust-policies.js +++ b/trust-policies.js @@ -1,44 +1,11 @@ /** - * @copyright 2023 Chris Zuber + * @copyright 2023-2024 Chris Zuber */ import { createPolicy } from './trust.js'; import { callOnce } from './utility.js'; -import { HTML } from '@shgysk8zer0/consts/mimes.js'; +import { attributes, elements, comments } from '@aegisjsproject/sanitizer/config/base.js'; export { getYouTubePolicy } from'./google/policies.js'; - -// @todo Remove use of `Sanitizer.getDefaultConfiguration()` -const { - allowElements, allowAttributes, allowComments, blockElements, dropAttributes, - dropElements, -} = Sanitizer.getDefaultConfiguration(); - -const unsafeElements = [ - 'script', 'title', 'noscript', 'object', 'embed', 'style', 'param', 'iframe', - 'base', 'frame', 'frameset', -]; - -const unsafeAttrs = [ - 'ping', 'action', 'formaction', 'http-equiv', 'background', 'style', 'bgcolor', - 'fgcolor', 'linkcolor', 'lowsrc', ...Object.keys( - Object.getOwnPropertyDescriptors(HTMLElement.prototype) - ).filter(desc => desc.startsWith('on')), -]; - -const urlAttrs = ['href', 'src', 'cite']; -const allowedProtocols = location.protocol === 'http:' ? ['http:', 'https:'] : ['https:']; - -function isUnsafeURL(attr, node) { - const val = node.getAttribute(attr); - return ( - typeof val === 'string' - && val.length !== 0 - && urlAttrs.includes(attr) - && URL.canParse(val, document.baseURI) - && ! allowedProtocols.includes(new URL(val, document.baseURI).protocol) - ); -} - /* * Do NOT export this! It is dangerous and MUST only be used internally. * But it still needs to be added to `trusted-types` in CSP if used. @@ -93,23 +60,11 @@ export function stripHTML(input) { return tmp.content.firstElementChild.textContent; } -export function sanitizeHTML(input, { - allowElements, allowComments, allowAttributes, allowCustomElements, - blockElements, dropAttributes, dropElements, allowUnknownMarkup, sanitizer, -} = {}) { +export function sanitizeHTML(input, { elements, attributes, comments } = {}) { if (Element.prototype.setHTML instanceof Function) { const el = document.createElement('div'); - el.setHTML(input, { - allowElements, allowComments, allowAttributes, allowCustomElements, - blockElements, dropAttributes, dropElements, allowUnknownMarkup, sanitizer, - }); + el.setHTML(input, { sanitizer: { elements, attributes, comments }}); return el.innerHTML; - } else if ( - 'Sanitizer' in globalThis - && Sanitizer.prototype.sanitizeFor instanceof Function - && typeof sanitizer === 'object' && sanitizer instanceof Sanitizer - ) { - return sanitizer.sanitizeFor('div', input).innerHTML; } else { console.warn('Sanitizer not supported. Returning escaped HTML'); return escapeHTML(input); @@ -131,86 +86,60 @@ export const createEmptyScript = () => trustedTypes.emptyScript; * @TODO: Add support for SVG */ export const sanitizerConfig = { - allowComments, - allowCustomElements: true, - allowElements: [ - ...allowElements, 'krv-ad', 'krv-events', 'leaflet-map', 'leaflet-marker', + comments, + elements: [ + ...elements, 'krv-ad', 'krv-events', 'leaflet-map', 'leaflet-marker', 'youtube-player', 'spotify-player', 'weather-current', 'weather-forecast', 'github-user', 'github-repo', 'github-gist', 'wfd-events', 'codepen-embed', 'bacon-ipsum', 'facebook-post', ], - allowAttributes: { - ...allowAttributes, - 'theme': [ - 'krv-ad', 'weather-current', 'weather-forecast', 'wfd-events', - 'codepen-embed', - ], - 'loading': [ - ...allowAttributes.loading, 'krv-ad', 'weather-current', - 'weather-forecast', 'youtube-player', 'spotify-player', - 'github-user', 'github-repo', 'wfd-events', 'codepen-embed', - ], - 'crossorigin': [...allowAttributes.crossorigin, 'leaflet-map'], - 'source': ['krv-ad', 'krv-events', 'wfd-events'], - 'medium': ['krv-ad', 'krv-events', 'wfd-events'], - 'content': ['krv-ad', 'krv-events', 'wfd-events'], - 'campaign': ['krv-ad', 'krv-events', 'wfd-events'], - 'term': ['krv-ad', 'krv-events', 'wfd-events'], - 'count': ['krv-events'], - 'layout': ['krv-ad'], - 'center': ['leaflet-map'], - 'zoom': ['leaflet-map'], - 'tilesrc': ['leflet-map'], - 'allowlocate': ['leaflet-map'], - 'allowfullscreen': ['leaflet-map'], - 'zoomcontrol': ['leaflet-map'], - 'minzoom': ['leaflet-map', 'leaflet-marker'], - 'maxzoom': ['leaflet-map', 'leaflet-marker'], - 'longitude': ['leaflet-marker'], - 'latitude': ['leaflet-marker'], - 'open': [...allowAttributes.open, 'krv-ad'], - 'appid': ['weather-current', 'weather-forecast'], - 'postalcode': ['weather-current', 'weather-forecast'], - 'height': [ - ...allowAttributes.height, 'youtube-player', 'github-gist', - 'codepen-embed', 'facebook-post', - ], - 'width': [ - ...allowAttributes.width, 'youtube-player', 'github-gist', - 'codepen-embed', 'facebook-post', - ], - 'large': ['spotify-player'], - 'uri': ['spotify-player'], - 'user': [ - 'github-repo', 'github-user', 'github-gist', 'codepen-embed', - 'facebook-post', - ], - 'bio': ['github-user'], - 'gist': ['github-gist'], - 'file': ['github-gist'], - 'repo': ['github-repo'], - 'pen': ['codepen-embed'], - 'tab': ['codepen-embed'], - 'editable': ['codepen-embed'], - 'clicktoload': ['codepen-embed'], - 'lines': ['bacon-ipsum'], - 'paras': ['bacon-ipsum'], - 'start-with-lorem': ['bacon-ipsum'], - 'filler': ['bacon-ipsum'], - 'post': ['facebook-post'], - 'showtext': ['facebook-post'], - }, - dropAttributes, - blockElements, - dropElements, + attributes: [ + ...attributes, + 'theme', + 'loading', + 'source', + 'medium', + 'content', + 'campaign', + 'term', + 'count', + 'layout', + 'center', + 'zoom', + 'tilesrc', + 'allowlocate', + 'allowfullscreen', + 'zoomcontrol', + 'minzoom', + 'maxzoom', + 'longitude', + 'latitude', + 'open', + 'appid', + 'postalcode', + 'large', + 'uri', + 'user', + 'bio', + 'gist', + 'file', + 'repo', + 'pen', + 'tab', + 'editable', + 'clicktoload', + 'lines', + 'paras', + 'start-with-lorem', + 'filler', + 'post', + 'showtext', + ], }; export const getDefaultPolicy = callOnce(() => { return createPolicy('default', { - createHTML: input => sanitizeHTML(input, { - allowElements, allowAttributes, allowComments, blockElements, - dropAttributes, dropElements, - }), + createHTML: input => sanitizeHTML(input,{ elements, attributes, comments }), createScript: createEmptyScript, createScriptURL: input => { if (isTrustedScriptOrigin(input)) { @@ -263,10 +192,7 @@ export const getDataScriptURLPolicy = callOnce(() => createPolicy('data#script-u // Disqus embeds are created via their script & must use default policy :( export const getDefaultPolicyWithDisqus = callOnce(() => { return createPolicy('default', { - createHTML: input => sanitizeHTML(input, { - allowElements, allowAttributes, allowComments, blockElements, dropAttributes, - dropElements, - }), + createHTML: input => sanitizeHTML(input, { elements, attributes, comments }), createScript: createEmptyScript, createScriptURL: input => { if (isTrustedScriptOrigin(input) || isDisqusEmbed(input)) { @@ -280,10 +206,7 @@ export const getDefaultPolicyWithDisqus = callOnce(() => { export const getKRVPolicy = callOnce(() => { return createPolicy('krv', { - createHTML: input => sanitizeHTML(input, { - allowElements, allowAttributes, allowComments, blockElements, dropAttributes, - dropElements, - }), + createHTML: input => sanitizeHTML(input, { elements, attributes, comments }), createScript: createEmptyScript, createScriptURL: input => { if (isTrustedScriptOrigin(input)) { @@ -317,44 +240,4 @@ export const getDisqusPolicy = callOnce(() => createPolicy('disqus#script-url', } })); -export function isSafeHTML(content) { - try { - const policy = getRawHTMLPolicy(); - const parsed = content instanceof Node - ? content - : new DOMParser().parseFromString(policy.createHTML(content), HTML); - - const iter = document.createNodeIterator(parsed, NodeFilter.SHOW_ELEMENT); - - let safe = true; - let node = iter.nextNode(); - - while (node instanceof Node) { - if (node.nodeType === Node.ELEMENT_NODE) { - if (unsafeElements.includes(node.localName.toLowerCase())) { - safe = false; - break; - } else if (node.getAttributeNames().some(attr => { - return unsafeAttrs.includes(attr.toLowerCase()) || isUnsafeURL(attr, node); - })) { - safe = false; - break; - } else if (node.tagName === 'TEMPLATE' && ! isSafeHTML(node.content)) { - safe = false; - break; - } else { - node = iter.nextNode(); - } - } else { - safe = false; - break; - } - } - - return safe; - } catch { - return false; - } -} - export const getDefaultNoOpPolicy = callOnce(() => createPolicy('default', {}));