diff --git a/.changeset/nice-buttons-return.md b/.changeset/nice-buttons-return.md new file mode 100644 index 0000000000..a47a3aa1a7 --- /dev/null +++ b/.changeset/nice-buttons-return.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': minor +--- + +Adds `additionalAllowedURIProtocols` to sanitizer config diff --git a/docs/features/techdocs/how-to-guides.md b/docs/features/techdocs/how-to-guides.md index 7d8c99ffda..755d88b163 100644 --- a/docs/features/techdocs/how-to-guides.md +++ b/docs/features/techdocs/how-to-guides.md @@ -606,6 +606,25 @@ techdocs: This way, custom element like `` will be allowed in the result HTML. +## How to allow additional URI protocols in TechDocs + +TechDocs uses the [DOMPurify](https://github.com/cure53/DOMPurify) library to +sanitize HTML and prevent XSS attacks. + +It's possible to allow additional URI protocols based on a list of protocols. To do +this, add the allowed protocols in the `techdocs.sanitizer.additionalAllowedURIProtocols` +and `additionalAllowedURIProtocols` configuration of your `app-config.yaml`. + +For example: + +```yaml +techdocs: + sanitizer: + additionalAllowedURIProtocols: ["vscode"], +``` + +This way, links like `VSCode Settings` 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 bfc98a7927..7294f52816 100644 --- a/plugins/techdocs/config.d.ts +++ b/plugins/techdocs/config.d.ts @@ -58,6 +58,16 @@ export interface Config { * @visibility frontend */ allowedCustomElementAttributeNameRegExp?: string; + /** + * Allows listed protocols in attributes with URI values + * Example: + * additionalAllowedURIProtocols: ['vscode'] + * this will allow all attributes with URI values to have `vscode` protocol like `vscode://some/path` in addition to the default protocols + * matched by DOMPurify's IS_ALLOWED_URI RegExp: + * @see: https://raw.githubusercontent.com/cure53/DOMPurify/master/src/regexp.ts + * @visibility frontend + */ + additionalAllowedURIProtocols?: 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 index f79a41a73d..39d3d229aa 100644 --- a/plugins/techdocs/src/reader/transformers/html/transformer.sanitizer.test.tsx +++ b/plugins/techdocs/src/reader/transformers/html/transformer.sanitizer.test.tsx @@ -28,6 +28,7 @@ const configApiMock: ConfigApi = new ConfigReader({ sanitizer: { allowedCustomElementTagNameRegExp: '^backstage-', allowedCustomElementAttributeNameRegExp: 'attribute1|attribute2', + additionalAllowedURIProtocols: ['permitted'], }, }, }); @@ -39,6 +40,28 @@ const wrapper: FC> = ({ children }) => ( ); describe('Transformers > Html > Sanitizer Custom Elements', () => { + it('allows additional protocols in URIs when provided via config', async () => { + const { result } = renderHook(() => useSanitizerTransformer(), { wrapper }); + const dirtyDom = document.createElement('html'); + + const dirtyHTML = ` + + Yep + Nope + Example + `; + dirtyDom.innerHTML = dirtyHTML; + + const clearDom = await result.current(dirtyDom); // calling html transformer + const elements = Array.from( + clearDom.querySelectorAll('body > a'), + ); + expect(elements).toHaveLength(3); + expect(elements[0].getAttribute('href')).toEqual('permitted:mcp/install'); + expect(elements[1].getAttribute('href')).toBeNull(); + expect(elements[2].getAttribute('href')).toEqual('https://example.com'); + }); + it('should return a function that allows custom elements matching the pattern in the given dom element', async () => { const { result } = renderHook(() => useSanitizerTransformer(), { wrapper }); diff --git a/plugins/techdocs/src/reader/transformers/html/transformer.ts b/plugins/techdocs/src/reader/transformers/html/transformer.ts index 42f34f0410..7c341c783e 100644 --- a/plugins/techdocs/src/reader/transformers/html/transformer.ts +++ b/plugins/techdocs/src/reader/transformers/html/transformer.ts @@ -78,6 +78,35 @@ export const useSanitizerTransformer = (): Transformer => { const attributeNameCheck = config?.getOptionalString( 'allowedCustomElementAttributeNameRegExp', ); + const additionalAllowedURIProtocols = + config?.getOptionalStringArray('additionalAllowedURIProtocols') || []; + + // Define allowed URI protocols, including any additional ones from the config. + // The default protocols are based on the DOMPurify defaults. + const allowedURIProtocols = [ + 'callto', + 'cid', + 'ftp', + 'ftps', + 'http', + 'https', + 'mailto', + 'matrix', + 'sms', + 'tel', + 'xmpp', + ...additionalAllowedURIProtocols, + ].filter(Boolean); + + const allowedURIRegExp = new RegExp( + // This regex is not exposed by DOMPurify, so we need to define it ourselves. + // It is possible for this to drift from the default in future versions of DOMPurify. + // See: https://raw.githubusercontent.com/cure53/DOMPurify/master/src/regexp.ts + `^(?:${allowedURIProtocols.join( + '|', + )}:|[^a-z]|[a-z+.-]+(?:[^a-z+.\\-:]|$))`, + 'i', + ); // using outerHTML as we want to preserve the html tag attributes (lang) return DOMPurify.sanitize(dom.outerHTML, { @@ -86,6 +115,7 @@ export const useSanitizerTransformer = (): Transformer => { ADD_ATTR: ['http-equiv', 'content', 'dominant-baseline'], WHOLE_DOCUMENT: true, RETURN_DOM: true, + ALLOWED_URI_REGEXP: allowedURIRegExp, CUSTOM_ELEMENT_HANDLING: { tagNameCheck: tagNameCheck ? new RegExp(tagNameCheck) : undefined, attributeNameCheck: attributeNameCheck