mirror of
https://github.com/l5yth/potato-mesh.git
synced 2026-08-06 17:03:27 +02:00
Harden instance selector navigation URLs (#550)
* Harden instance selector navigation URLs * Cover malformed instance URL handling
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
Reference in New Issue
Block a user