diff --git a/.changeset/great-adults-stare.md b/.changeset/great-adults-stare.md
new file mode 100644
index 0000000000..05d196ac62
--- /dev/null
+++ b/.changeset/great-adults-stare.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-techdocs': minor
+---
+
+Ensure that techdocs rewritten URLs do not contain potentially dangerous javascript: URLs.
diff --git a/plugins/techdocs/src/reader/transformers/rewriteDocLinks.test.ts b/plugins/techdocs/src/reader/transformers/rewriteDocLinks.test.ts
index 66fc5f29c1..864129164c 100644
--- a/plugins/techdocs/src/reader/transformers/rewriteDocLinks.test.ts
+++ b/plugins/techdocs/src/reader/transformers/rewriteDocLinks.test.ts
@@ -71,6 +71,33 @@ describe('rewriteDocLinks', () => {
expect(getSample(shadowDom, 'a', 'href')).toEqual([]);
expect(shadowDom.innerHTML).toContain(expectedText);
});
+
+ it('should rewrite javascript hrefs as text', async () => {
+ const samples: Array<[string, string]> = [
+ // eslint-disable-next-line no-script-url
+ ['javascript:alert(1)', 'JS 1'],
+ [' javascript:alert(2)', 'JS 2 (leading space)'],
+ ['\n\tjavascript:alert(3)', 'JS 3 (whitespace)'],
+ // eslint-disable-next-line no-script-url
+ ['JaVaScRiPt:alert(4)', 'JS 4 (mixed case)'],
+ ['javascript:alert(5)', 'JS 5 (entity-encoded colon)'],
+ ];
+
+ const html = samples
+ .map(([href, text]) => `${text}`)
+ .join('\n');
+
+ const shadowDom = await createTestShadowDom(html, {
+ preTransformers: [rewriteDocLinks()],
+ postTransformers: [],
+ });
+
+ // There should be no tags, but the link text should remain.
+ expect(getSample(shadowDom, 'a', 'href')).toEqual([]);
+ for (const [, text] of samples) {
+ expect(shadowDom.innerHTML).toContain(text);
+ }
+ });
});
describe('normalizeUrl', () => {
diff --git a/plugins/techdocs/src/reader/transformers/rewriteDocLinks.ts b/plugins/techdocs/src/reader/transformers/rewriteDocLinks.ts
index e43dda1815..a1005fe1d6 100644
--- a/plugins/techdocs/src/reader/transformers/rewriteDocLinks.ts
+++ b/plugins/techdocs/src/reader/transformers/rewriteDocLinks.ts
@@ -16,6 +16,11 @@
import type { Transformer } from './transformer';
+// See https://github.com/facebook/react/blob/f0cf832e1d0c8544c36aa8b310960885a11a847c/packages/react-dom-bindings/src/shared/sanitizeURL.js
+const scriptProtocolPattern =
+ // eslint-disable-next-line no-control-regex
+ /^[\u0000-\u001F ]*j[\r\n\t]*a[\r\n\t]*v[\r\n\t]*a[\r\n\t]*s[\r\n\t]*c[\r\n\t]*r[\r\n\t]*i[\r\n\t]*p[\r\n\t]*t[\r\n\t]*\:/i;
+
export const rewriteDocLinks = (): Transformer => {
return dom => {
const updateDom = (
@@ -33,6 +38,12 @@ export const rewriteDocLinks = (): Transformer => {
}
try {
+ if (scriptProtocolPattern.test(elemAttribute)) {
+ throw new TypeError(
+ `Invalid location ref '${elemAttribute}', target is a javascript: URL`,
+ );
+ }
+
const normalizedWindowLocation = normalizeUrl(
window.location.href,
);