Apply fixes

Signed-off-by: Artem Savchenko <armisav@gmail.com>
This commit is contained in:
Artem Savchenko
2026-06-16 14:20:21 +07:00
parent dfd93541c8
commit 68cbf8a886
3 changed files with 22 additions and 4 deletions
@@ -98,6 +98,16 @@ describe('isBlockedHost', () => {
it('handles bracketed and trailing-dot hostnames', () => {
expect(isBlockedHost('[::1]')).toBe(true)
expect(isBlockedHost('localhost.')).toBe(true)
expect(isBlockedHost('localhost...')).toBe(true)
expect(isBlockedHost('127.0.0.1.')).toBe(true)
expect(isBlockedHost('example.com.')).toBe(false)
})
it('strips trailing dots in linear time on hostile input (ReDoS regression)', () => {
const hostile = '.'.repeat(100000) + 'x'
const start = Date.now()
expect(isBlockedHost(hostile)).toBe(false)
expect(Date.now() - start).toBeLessThan(1000)
})
})
+8 -3
View File
@@ -62,9 +62,14 @@ const DEFAULT_ALLOWED_PROTOCOLS = ['http:', 'https:']
function normalizeHostnameForChecks (hostname: string): string {
// URL.hostname is already punycode-normalized by WHATWG URL for IDNs.
// Keep it lowercase for comparisons.
const trimmed = hostname.trim().toLowerCase().replace(/\.+$/, '')
if (trimmed.startsWith('[') && trimmed.endsWith(']')) return trimmed.slice(1, -1)
return trimmed
let host = hostname.trim().toLowerCase()
// Strip trailing dots with a linear scan rather than /\.+$/, which backtracks
// in polynomial time on hostile input (e.g. many '.' followed by a non-dot).
let end = host.length
while (end > 0 && host.charCodeAt(end - 1) === 0x2e /* '.' */) end--
host = host.slice(0, end)
if (host.startsWith('[') && host.endsWith(']')) return host.slice(1, -1)
return host
}
// Expands an IPv6 literal (possibly compressed, zone-suffixed, or carrying an
+4 -1
View File
@@ -688,8 +688,11 @@ export function start (
return cookie !== undefined ? { lookup, headers: { Cookie: cookie } } : { lookup }
} catch (err) {
if (err instanceof SsrfError) {
// Log the detail (incl. the offending url) server-side, but never reflect it
// back: res.send(string) is served as text/html, so echoing err.message would
// be a reflected-XSS / info-leak sink. err.code is a fixed enum, safe to return.
ctx.warn('import blocked', { code: err.code, url })
res.status(400).send(err.message)
res.status(400).send(`Import URL rejected: ${err.code}`)
return undefined
}
throw err