From 074ecf9bc905ec9df2e0526d6beaf2e62d2711ca Mon Sep 17 00:00:00 2001 From: liyachen Date: Sat, 18 Jul 2026 18:15:20 -0400 Subject: [PATCH] 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 --- src/util.ts | 2 +- tests/util.test.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/util.ts b/src/util.ts index 8d24195..0db0d13 100644 --- a/src/util.ts +++ b/src/util.ts @@ -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 diff --git a/tests/util.test.ts b/tests/util.test.ts index cd25bd3..f1e1662 100644 --- a/tests/util.test.ts +++ b/tests/util.test.ts @@ -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(); });