Harden instance selector navigation URLs (#550)

* Harden instance selector navigation URLs

* Cover malformed instance URL handling
This commit is contained in:
l5y
2025-12-14 18:40:41 +01:00
committed by GitHub
parent 0e211aebdd
commit b5eecb1ec1
2 changed files with 51 additions and 8 deletions
@@ -90,10 +90,29 @@ test('resolveInstanceLabel falls back to the domain when the name is missing', (
test('buildInstanceUrl normalises domains into navigable HTTPS URLs', () => {
assert.equal(buildInstanceUrl('mesh.example'), 'https://mesh.example');
assert.equal(buildInstanceUrl(' https://mesh.example '), 'https://mesh.example');
assert.equal(buildInstanceUrl('https://mesh.example/path?query#fragment'), 'https://mesh.example');
assert.equal(buildInstanceUrl('javascript:alert(1)'), null);
assert.equal(buildInstanceUrl('ftp://mesh.example'), null);
assert.equal(buildInstanceUrl('mesh.example:8080'), 'https://mesh.example:8080');
assert.equal(buildInstanceUrl('mesh.example<script>'), null);
assert.equal(buildInstanceUrl(''), null);
assert.equal(buildInstanceUrl(null), null);
});
test('buildInstanceUrl rejects malformed HTTP URLs safely', () => {
const originalWarn = console.warn;
const warnings = [];
console.warn = message => warnings.push(message);
try {
assert.equal(buildInstanceUrl('http://[::1'), null);
assert.equal(buildInstanceUrl('https://bad host.example'), null);
assert.ok(warnings.length >= 1);
} finally {
console.warn = originalWarn;
}
});
test('initializeInstanceSelector populates options alphabetically and selects the configured domain', async () => {
const env = createDomEnvironment();
const select = setupSelectElement(env.document);
+32 -8
View File
@@ -34,12 +34,15 @@ function resolveInstanceLabel(entry) {
return domain;
}
/**
* Construct a navigable URL for the provided instance domain.
*
* @param {string} domain Instance domain as returned by the federation catalog.
* @returns {string|null} Navigable absolute URL or ``null`` when the domain is empty.
*/
/**
* Construct a navigable URL for the provided instance domain.
*
* The returned URL is guaranteed to use HTTP(S) and a host-only component to avoid
* interpreting arbitrary DOM-controlled text as executable content.
*
* @param {string} domain Instance domain as returned by the federation catalog.
* @returns {string|null} Navigable absolute URL or ``null`` when the domain is empty or unsafe.
*/
export function buildInstanceUrl(domain) {
if (typeof domain !== 'string') {
return null;
@@ -50,8 +53,29 @@ export function buildInstanceUrl(domain) {
return null;
}
if (/^[a-zA-Z][a-zA-Z\d+.-]*:\/\//.test(trimmed)) {
return trimmed;
const allowedHostPattern = /^[a-zA-Z0-9.-]+(?::\d{1,5})?$/;
if (/^https?:\/\//i.test(trimmed)) {
try {
const parsed = new URL(trimmed);
if (!['http:', 'https:'].includes(parsed.protocol)) {
return null;
}
const sanitizedHost = parsed.host.trim();
if (!allowedHostPattern.test(sanitizedHost)) {
return null;
}
return `${parsed.protocol}//${sanitizedHost}`;
} catch (error) {
console.warn('Rejected invalid instance URL', error);
return null;
}
}
if (!allowedHostPattern.test(trimmed)) {
return null;
}
return `https://${trimmed}`;