From 4f0cb89c4284d77ce6f1f6e3c396756d6d038133 Mon Sep 17 00:00:00 2001 From: Harsha Teja Kanna Date: Thu, 17 Oct 2024 13:21:56 -0400 Subject: [PATCH] Add DomPurify sanitizer custom elements configuration (#26989) * Add DomPurify sanitizer custom elements configuration --------- Signed-off-by: Harsha Teja Kanna --- .changeset/red-fishes-press.md | 6 ++ docs/features/techdocs/how-to-guides.md | 20 ++++ plugins/techdocs/config.d.ts | 16 +++ .../html/transformer.sanitizer.test.tsx | 102 ++++++++++++++++++ .../reader/transformers/html/transformer.ts | 13 +++ 5 files changed, 157 insertions(+) create mode 100644 .changeset/red-fishes-press.md create mode 100644 plugins/techdocs/src/reader/transformers/html/transformer.sanitizer.test.tsx diff --git a/.changeset/red-fishes-press.md b/.changeset/red-fishes-press.md new file mode 100644 index 0000000000..8493ddf446 --- /dev/null +++ b/.changeset/red-fishes-press.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +Added DomPurify sanitizer configuration for custom elements implementing RFC https://github.com/backstage/backstage/issues/26988. +See https://backstage.io/docs/features/techdocs/how-to-guides#how-to-enable-custom-elements-in-techdocs for how to enable it in the configuration. diff --git a/docs/features/techdocs/how-to-guides.md b/docs/features/techdocs/how-to-guides.md index fd4d208b74..f8be6c9dd5 100644 --- a/docs/features/techdocs/how-to-guides.md +++ b/docs/features/techdocs/how-to-guides.md @@ -545,6 +545,26 @@ techdocs: This way, all iframes where the host in the src attribute is in the `sanitizer.allowedIframeHosts` list will be displayed. +## How to enable custom elements in TechDocs + +TechDocs uses the [DOMPurify](https://github.com/cure53/DOMPurify) library to +sanitize HTML and prevent XSS attacks. + +It's possible to allow custom elements based on a list of allowed patterns. To do +this, add the allowed elements and attributes in the `techdocs.sanitizer.allowedCustomElementTagNameRegExp` +and `allowedCustomElementAttributeNameRegExp` configuration of your `app-config.yaml`. + +For example: + +```yaml +techdocs: + sanitizer: + allowedCustomElementTagNameRegExp: '^backstage-', + allowedCustomElementAttributeNameRegExp: 'attribute1|attribute2', +``` + +This way, custom element like `` will be allowed in the result HTML. + ## How to render PlantUML diagram in TechDocs PlantUML allows you to create diagrams from plain text language. Each diagram description begins with the keyword - (@startXYZ and @endXYZ, depending on the kind of diagram). For UML Diagrams, Keywords @startuml & @enduml should be used. Further details for all types of diagrams can be found at [PlantUML Language Reference Guide](https://plantuml.com/guide). diff --git a/plugins/techdocs/config.d.ts b/plugins/techdocs/config.d.ts index 0df852e490..6103f273b5 100644 --- a/plugins/techdocs/config.d.ts +++ b/plugins/techdocs/config.d.ts @@ -42,6 +42,22 @@ export interface Config { * @visibility frontend */ allowedIframeHosts?: string[]; + /** + * Allows listed custom element tag name regex + * Example: + * allowedCustomElementTagNameRegExp: '^backstage-' + * this will allow all custom elements with tag name matching `^backstage-` like etc. + * @visibility frontend + */ + allowedCustomElementTagNameRegExp: string; + /** + * Allows listed custom element attribute name regex + * Example: + * allowedCustomElementAttributeNameRegExp: 'attribute1|attribute2' + * this will allow all custom element attributes matching `attribute1` or `attribute2` like + * @visibility frontend + */ + allowedCustomElementAttributeNameRegExp: string; }; }; } diff --git a/plugins/techdocs/src/reader/transformers/html/transformer.sanitizer.test.tsx b/plugins/techdocs/src/reader/transformers/html/transformer.sanitizer.test.tsx new file mode 100644 index 0000000000..a26757764c --- /dev/null +++ b/plugins/techdocs/src/reader/transformers/html/transformer.sanitizer.test.tsx @@ -0,0 +1,102 @@ +/* + * Copyright 2022 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 React, { FC, PropsWithChildren } from 'react'; +import { renderHook } from '@testing-library/react'; + +import { ConfigReader } from '@backstage/core-app-api'; +import { ConfigApi, configApiRef } from '@backstage/core-plugin-api'; +import { TestApiProvider } from '@backstage/test-utils'; + +import { useSanitizerTransformer } from './transformer'; + +const configApiMock: ConfigApi = new ConfigReader({ + techdocs: { + sanitizer: { + allowedCustomElementTagNameRegExp: '^backstage-', + allowedCustomElementAttributeNameRegExp: 'attribute1|attribute2', + }, + }, +}); + +const wrapper: FC> = ({ children }) => ( + + {children} + +); + +describe('Transformers > Html > Sanitizer Custom Elements', () => { + it('should return a function that allows custom elements matching the pattern in the given dom element', async () => { + const { result } = renderHook(() => useSanitizerTransformer(), { wrapper }); + + const dirtyDom = document.createElement('html'); + dirtyDom.innerHTML = ` + + + + `; + const clearDom = await result.current(dirtyDom); // calling html transformer + + const elements = Array.from( + clearDom.querySelectorAll('body > backstage-element'), + ); + expect(elements).toHaveLength(1); + expect(elements[0].hasAttribute('attribute1')).toEqual(true); + expect(elements[0].hasAttribute('attribute2')).toEqual(true); + }); + + it('should return a function that removes custom elements not matching the pattern in the given dom element', async () => { + const { result } = renderHook(() => useSanitizerTransformer(), { wrapper }); + + const dirtyDom = document.createElement('html'); + dirtyDom.innerHTML = ` + + + + + `; + const clearDom = await result.current(dirtyDom); // calling html transformer + + const elements = Array.from( + clearDom.querySelectorAll('body > backstage-element'), + ); + expect(elements).toHaveLength(1); + expect(elements[0].hasAttribute('attribute1')).toEqual(true); + expect(elements[0].hasAttribute('attribute2')).toEqual(true); + }); + + it('should return a function that removes custom element attributes not matching the pattern in the given dom element', async () => { + const { result } = renderHook(() => useSanitizerTransformer(), { wrapper }); + + const dirtyDom = document.createElement('html'); + dirtyDom.innerHTML = ` + + + + + `; + const clearDom = await result.current(dirtyDom); // calling html transformer + + const elements = Array.from( + clearDom.querySelectorAll('body > backstage-element'), + ); + expect(elements).toHaveLength(2); + expect(elements[0].hasAttribute('attribute1')).toEqual(true); + expect(elements[0].hasAttribute('attribute2')).toEqual(true); + expect(elements[1].hasAttribute('attribute3')).toEqual(false); + expect(elements[1].hasAttribute('attribute4')).toEqual(false); + }); +}); diff --git a/plugins/techdocs/src/reader/transformers/html/transformer.ts b/plugins/techdocs/src/reader/transformers/html/transformer.ts index 4938a36648..2004f941e3 100644 --- a/plugins/techdocs/src/reader/transformers/html/transformer.ts +++ b/plugins/techdocs/src/reader/transformers/html/transformer.ts @@ -72,6 +72,13 @@ export const useSanitizerTransformer = (): Transformer => { } }); + const tagNameCheck = config?.getOptionalString( + 'allowedCustomElementTagNameRegExp', + ); + const attributeNameCheck = config?.getOptionalString( + 'allowedCustomElementAttributeNameRegExp', + ); + // using outerHTML as we want to preserve the html tag attributes (lang) return DOMPurify.sanitize(dom.outerHTML, { ADD_TAGS: tags, @@ -79,6 +86,12 @@ export const useSanitizerTransformer = (): Transformer => { ADD_ATTR: ['http-equiv', 'content'], WHOLE_DOCUMENT: true, RETURN_DOM: true, + CUSTOM_ELEMENT_HANDLING: { + tagNameCheck: tagNameCheck ? new RegExp(tagNameCheck) : undefined, + attributeNameCheck: attributeNameCheck + ? new RegExp(attributeNameCheck) + : undefined, + }, }); }, [config],