fix: close trailing-dot bypass in private-host check

new URL() keeps the trailing dot on FQDN hostnames ('localhost.',
'printer.local.') while stripping it from IPv4, so those slipped past
the loopback/.local literal checks. Strip one trailing dot before the
compares. Found in final whole-branch review.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
liyachen 2026-07-18 18:15:20 -04:00
parent 99f8435e41
commit 074ecf9bc9
2 changed files with 3 additions and 1 deletions

View file

@ -26,7 +26,7 @@ export function sanitize(str: string): string {
// IPs are accepted — DNS-rebinding protection is out of scope for a desktop
// plugin talking to public CDNs.
function isPrivateHost(hostname: string): boolean {
let h = hostname.replace(/^\[|\]$/g, '').toLowerCase();
let h = hostname.replace(/^\[|\]$/g, '').toLowerCase().replace(/\.$/, '');
if (h === 'localhost' || h.endsWith('.local')) return true;
if (h.startsWith('::ffff:')) {
// IPv4-mapped IPv6; the URL parser serializes it as hex groups

View file

@ -121,6 +121,8 @@ describe('assertDownloadable', () => {
expect(() => assertDownloadable('http://127.0.0.1:8080/x')).toThrow();
expect(() => assertDownloadable('http://127.8.9.10/x')).toThrow();
expect(() => assertDownloadable('http://[::1]/x')).toThrow();
expect(() => assertDownloadable('http://localhost./x')).toThrow(); // FQDN trailing dot
expect(() => assertDownloadable('http://printer.local./x')).toThrow();
expect(() => assertDownloadable('http://0x7f000001/x')).toThrow(); // URL-normalized to 127.0.0.1
expect(() => assertDownloadable('http://[::ffff:127.0.0.1]/x')).toThrow();
});