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