From f63b6754045dd574ac175c79a3597055c4bd2cec Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Mon, 23 May 2022 08:35:47 +0200 Subject: [PATCH] refactor(techdocs): delete sanitize dom transformer Signed-off-by: Camila Belo --- .../techdocs/src/reader/transformers/index.ts | 1 - .../reader/transformers/sanitizeDOM.test.ts | 254 ------------------ .../src/reader/transformers/sanitizeDOM.ts | 87 ------ 3 files changed, 342 deletions(-) delete mode 100644 plugins/techdocs/src/reader/transformers/sanitizeDOM.test.ts delete mode 100644 plugins/techdocs/src/reader/transformers/sanitizeDOM.ts diff --git a/plugins/techdocs/src/reader/transformers/index.ts b/plugins/techdocs/src/reader/transformers/index.ts index 748d702fdb..bc72f2d78f 100644 --- a/plugins/techdocs/src/reader/transformers/index.ts +++ b/plugins/techdocs/src/reader/transformers/index.ts @@ -24,7 +24,6 @@ export * from './copyToClipboard'; export * from './removeMkdocsHeader'; export * from './simplifyMkdocsFooter'; export * from './onCssReady'; -export * from './sanitizeDOM'; export * from './injectCss'; export * from './scrollIntoAnchor'; export * from './transformer'; diff --git a/plugins/techdocs/src/reader/transformers/sanitizeDOM.test.ts b/plugins/techdocs/src/reader/transformers/sanitizeDOM.test.ts deleted file mode 100644 index 697b01259c..0000000000 --- a/plugins/techdocs/src/reader/transformers/sanitizeDOM.test.ts +++ /dev/null @@ -1,254 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ConfigReader } from '@backstage/config'; -import { createTestShadowDom, FIXTURES } from '../../test-utils'; -import { Transformer } from './index'; -import { sanitizeDOM } from './sanitizeDOM'; - -const injectMaliciousLink = (): Transformer => dom => { - const link = document.createElement('a'); - link.setAttribute('id', 'test-malicious-link'); - link.setAttribute('onclick', 'alert("Hello world");'); - dom.querySelector('body')?.appendChild(link); - return dom; -}; - -describe('sanitizeDOM', () => { - it('contains a script tag', async () => { - const shadowDom = await createTestShadowDom(FIXTURES.FIXTURE_STANDARD_PAGE); - - expect(shadowDom.querySelectorAll('script').length).toBeGreaterThan(0); - }); - - it('does not contain a script tag', async () => { - const shadowDom = await createTestShadowDom( - FIXTURES.FIXTURE_STANDARD_PAGE, - { - preTransformers: [sanitizeDOM()], - postTransformers: [], - }, - ); - - expect(shadowDom.querySelectorAll('script').length).toBe(0); - }); - - it('contains link with a onClick attribute', async () => { - const shadowDom = await createTestShadowDom( - FIXTURES.FIXTURE_STANDARD_PAGE, - { - preTransformers: [injectMaliciousLink()], - postTransformers: [], - }, - ); - - expect( - shadowDom.querySelector('#test-malicious-link')?.hasAttribute('onclick'), - ).toBeTruthy(); - }); - - it('does not contain link with a onClick attribute', async () => { - const shadowDom = await createTestShadowDom( - FIXTURES.FIXTURE_STANDARD_PAGE, - { - preTransformers: [sanitizeDOM()], - postTransformers: [], - }, - ); - - expect( - shadowDom.querySelector('#test-malicious-link')?.hasAttribute('onclick'), - ).toBeFalsy(); - }); - - it('removes style tags', async () => { - const html = ` - - - - - - - - `; - - const shadowDom = await createTestShadowDom(html, { - preTransformers: [sanitizeDOM()], - postTransformers: [], - }); - - expect(shadowDom.querySelectorAll('style').length).toEqual(0); - }); - - it('does not remove link tags', async () => { - const html = ` - - - - - - - - `; - - const shadowDom = await createTestShadowDom(html, { - preTransformers: [sanitizeDOM()], - postTransformers: [], - }); - - expect(shadowDom.querySelectorAll('link').length).toEqual(1); - }); - - it('render iframe where src host is in allowedIframeHosts', async () => { - const html = ` - - - - - - - - - - `; - const config = new ConfigReader({ - allowedIframeHosts: ['example.com'], - }); - const shadowDom = await createTestShadowDom(html, { - preTransformers: [sanitizeDOM(config)], - postTransformers: [], - }); - expect(shadowDom.querySelectorAll('link').length).toEqual(1); - expect(shadowDom.querySelectorAll('iframe').length).toEqual(1); - expect(shadowDom.querySelectorAll('iframe')[0].getAttribute('src')).toBe( - 'https://example.com?test=1', - ); - }); - - it('should remove all iframes without allowedIframeHosts', async () => { - const html = ` - - - - - - - - - - `; - const config = new ConfigReader({}); - const shadowDom = await createTestShadowDom(html, { - preTransformers: [sanitizeDOM(config)], - postTransformers: [], - }); - expect(shadowDom.querySelectorAll('link').length).toEqual(1); - expect(shadowDom.querySelectorAll('iframe').length).toEqual(0); - }); - - it('should remove iframe with invalid url in src', async () => { - const html = ` - - - - - - - - - `; - const config = new ConfigReader({ - allowedIframeHosts: ['example.com'], - }); - const shadowDom = await createTestShadowDom(html, { - preTransformers: [sanitizeDOM(config)], - postTransformers: [], - }); - expect(shadowDom.querySelectorAll('link').length).toEqual(1); - expect(shadowDom.querySelectorAll('iframe').length).toEqual(0); - }); - - test.each([ - { key: 'allow', value: '"camera \'none\'"', allowed: false }, - { key: 'allowfullscreen', value: true, allowed: false }, - { key: 'allowpaymentrequest', value: true, allowed: false }, - { key: 'height', value: true, allowed: true }, - { key: 'loading', value: "'lazy'", allowed: true }, - { key: 'name', value: "'example'", allowed: true }, - { key: 'referrerpolicy', value: "'no-referrer'", allowed: false }, - { key: 'sandbox', value: "'allow-forms'", allowed: false }, - { key: 'srcdoc', value: "'

Hello world!

'", allowed: false }, - { key: 'onload', value: "'alert(1)'", allowed: false }, - ])('check if the iframe has the attribute %p', async attr => { - const html = ` - - - - - - - - - `; - const config = new ConfigReader({ - allowedIframeHosts: ['example.com'], - }); - const shadowDom = await createTestShadowDom(html, { - preTransformers: [sanitizeDOM(config)], - postTransformers: [], - }); - expect(shadowDom.querySelectorAll('link').length).toEqual(1); - expect(shadowDom.querySelectorAll('iframe').length).toEqual(1); - expect(shadowDom.querySelectorAll('iframe')[0].hasAttribute(attr.key)).toBe( - attr.allowed, - ); - }); - - describe('safe head links', () => { - let shadowDom: ShadowRoot; - - beforeEach(async () => { - shadowDom = await createTestShadowDom(FIXTURES.FIXTURE_STANDARD_PAGE, { - preTransformers: [sanitizeDOM()], - postTransformers: [], - }); - }); - - it('should not sanitize the techdocs css', async () => { - const techdocsCss = shadowDom.querySelector( - 'link[href$="main.fe0cca5b.min.css"]', - ); - const rel = techdocsCss!.getAttribute('rel'); - expect(rel).toBe('stylesheet'); - }); - - it('should not sanitize google fonts', async () => { - const googleFonts = shadowDom.querySelector( - 'link[href^="https://fonts.googleapis.com"]', - ); - const rel = googleFonts!.getAttribute('rel'); - expect(rel).toBe('stylesheet'); - }); - - it('should not sanitize gstatic fonts', async () => { - const gstaticFonts = shadowDom.querySelector( - 'link[href^="https://fonts.gstatic.com"]', - ); - const rel = gstaticFonts!.getAttribute('rel'); - expect(rel).toBe('preconnect'); - }); - }); -}); diff --git a/plugins/techdocs/src/reader/transformers/sanitizeDOM.ts b/plugins/techdocs/src/reader/transformers/sanitizeDOM.ts deleted file mode 100644 index da4bd793be..0000000000 --- a/plugins/techdocs/src/reader/transformers/sanitizeDOM.ts +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -const TECHDOCS_CSS = /main\.[A-Fa-f0-9]{8}\.min\.css$/; -const GOOGLE_FONTS = /^https:\/\/fonts\.googleapis\.com/; -const GSTATIC_FONTS = /^https:\/\/fonts\.gstatic\.com/; - -export const safeLinksHook = (node: Element) => { - if (node.nodeName && node.nodeName === 'LINK') { - const href = node.getAttribute('href') || ''; - if (href.match(TECHDOCS_CSS)) { - node.setAttribute('rel', 'stylesheet'); - } - if (href.match(GOOGLE_FONTS)) { - node.setAttribute('rel', 'stylesheet'); - } - if (href.match(GSTATIC_FONTS)) { - node.setAttribute('rel', 'preconnect'); - } - } - return node; -}; - -const filterIframeHook = (allowedIframeHosts: string[]) => (node: Element) => { - if (node.nodeName === 'IFRAME') { - const src = node.getAttribute('src'); - if (!src) { - node.remove(); - return node; - } - - try { - const srcUrl = new URL(src); - const isMatch = allowedIframeHosts.some(host => srcUrl.host === host); - if (!isMatch) { - node.remove(); - } - } catch (error) { - // eslint-disable-next-line no-console - console.warn(`Invalid iframe src, ${error}`); - node.remove(); - } - } - return node; -}; - -import { Config } from '@backstage/config'; -import DOMPurify from 'dompurify'; -import type { Transformer } from './transformer'; - -export const sanitizeDOM = (config?: Config): Transformer => { - const allowedIframeHosts = - config?.getOptionalStringArray('allowedIframeHosts') || []; - - return dom => { - DOMPurify.addHook('afterSanitizeAttributes', safeLinksHook); - const addTags = ['link']; - - if (allowedIframeHosts.length > 0) { - DOMPurify.addHook( - 'beforeSanitizeElements', - filterIframeHook(allowedIframeHosts), - ); - addTags.push('iframe'); - } - - return DOMPurify.sanitize(dom.innerHTML, { - ADD_TAGS: addTags, - FORBID_TAGS: ['style'], - WHOLE_DOCUMENT: true, - RETURN_DOM: true, - }); - }; -};