techdocs: Disallow javascript URLs

Signed-off-by: Joshua Rogers <MegaManSec@users.noreply.github.com>
This commit is contained in:
Joshua Rogers
2025-08-05 11:18:09 +02:00
parent 5ae0d6849b
commit 067fdcd0a5
3 changed files with 43 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-techdocs': minor
---
Ensure that techdocs rewritten URLs do not contain potentially dangerous javascript: URLs.
@@ -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&#x3A;alert(5)', 'JS 5 (entity-encoded colon)'],
];
const html = samples
.map(([href, text]) => `<a href="${href}">${text}</a>`)
.join('\n');
const shadowDom = await createTestShadowDom(html, {
preTransformers: [rewriteDocLinks()],
postTransformers: [],
});
// There should be no <a> 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', () => {
@@ -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 = <T extends Element>(
@@ -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,
);