fix(frontend): preserve absolute and protocol-relative URLs in ensureAppRoot (#38316)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Joe Li
2026-03-05 14:06:16 -08:00
committed by GitHub
parent 357e35dc62
commit 6c359733e1
3 changed files with 213 additions and 10 deletions

View File

@@ -158,3 +158,74 @@ test('makeUrl should handle URLs with anchors', async () => {
'/superset/dashboard/123#anchor',
);
});
// Representative URLs used across the absolute-URL passthrough tests below.
const HTTPS_URL = 'https://external.example.com';
const HTTP_URL = 'http://external.example.com';
const PROTOCOL_RELATIVE_URL = '//external.example.com';
const FTP_URL = 'ftp://files.example.com/data';
const MAILTO_URL = 'mailto:user@example.com';
const TEL_URL = 'tel:+1234567890';
// Sets up bootstrap data and returns a fresh pathUtils module instance.
// Passing appRoot='' (default) simulates no subdirectory deployment.
async function loadPathUtils(appRoot = '') {
const bootstrapData = { common: { application_root: appRoot } };
document.body.innerHTML = `<div id="app" data-bootstrap='${JSON.stringify(bootstrapData)}'></div>`;
jest.resetModules();
await import('./getBootstrapData');
return import('./pathUtils');
}
test('ensureAppRoot should preserve absolute and protocol-relative URLs unchanged with default root', async () => {
const { ensureAppRoot } = await loadPathUtils();
expect(ensureAppRoot(HTTPS_URL)).toBe(HTTPS_URL);
expect(ensureAppRoot(HTTP_URL)).toBe(HTTP_URL);
expect(ensureAppRoot(PROTOCOL_RELATIVE_URL)).toBe(PROTOCOL_RELATIVE_URL);
});
test('ensureAppRoot should preserve absolute URLs unchanged with custom subdirectory', async () => {
const { ensureAppRoot } = await loadPathUtils('/superset/');
expect(ensureAppRoot(HTTPS_URL)).toBe(HTTPS_URL);
expect(ensureAppRoot(HTTP_URL)).toBe(HTTP_URL);
// Non-http absolute schemes: all safe schemes must pass through
expect(ensureAppRoot(FTP_URL)).toBe(FTP_URL);
expect(ensureAppRoot(MAILTO_URL)).toBe(MAILTO_URL);
expect(ensureAppRoot(TEL_URL)).toBe(TEL_URL);
});
test('ensureAppRoot should preserve protocol-relative URLs unchanged', async () => {
const { ensureAppRoot } = await loadPathUtils('/superset/');
expect(ensureAppRoot(PROTOCOL_RELATIVE_URL)).toBe(PROTOCOL_RELATIVE_URL);
});
test('makeUrl should preserve absolute and protocol-relative URLs unchanged', async () => {
const { makeUrl } = await loadPathUtils('/superset/');
expect(makeUrl(HTTPS_URL)).toBe(HTTPS_URL);
expect(makeUrl(PROTOCOL_RELATIVE_URL)).toBe(PROTOCOL_RELATIVE_URL);
// Non-http absolute scheme parity with ensureAppRoot
expect(makeUrl(FTP_URL)).toBe(FTP_URL);
});
test('ensureAppRoot should block javascript: and data: schemes (XSS prevention)', async () => {
const { ensureAppRoot } = await loadPathUtils('/superset/');
// Dangerous schemes must NOT pass through — they get prefixed to neutralise them.
// Build the literals via concatenation so the linter's no-script-url rule
// does not flag this intentional test input.
const jsUrl = `${'javascript'}:alert(1)`;
const dataUrl = `${'data'}:text/html,<h1>xss</h1>`;
expect(ensureAppRoot(jsUrl)).toBe(`/superset/${jsUrl}`);
expect(ensureAppRoot(dataUrl)).toBe(`/superset/${dataUrl}`);
});
test('ensureAppRoot should prefix unknown schemes instead of passing through', async () => {
const { ensureAppRoot } = await loadPathUtils('/superset/');
// Unknown / custom schemes are treated as relative paths
expect(ensureAppRoot('foo:bar')).toBe('/superset/foo:bar');
});