From a0549742879276361a74bbb4cc91c95a668c7a01 Mon Sep 17 00:00:00 2001 From: Luis Montes Date: Fri, 6 Feb 2026 23:57:05 -0700 Subject: [PATCH 1/2] fix(security): CVE-HSYNC-2026-006 - Comprehensive host validation --- lib/socket-listeners.js | 66 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/lib/socket-listeners.js b/lib/socket-listeners.js index 496faad..10a0b1e 100644 --- a/lib/socket-listeners.js +++ b/lib/socket-listeners.js @@ -9,6 +9,67 @@ debugError.color = 1; let net; +// Localhost aliases that all refer to the same machine +const LOCALHOST_ALIASES = new Set([ + 'localhost', + '127.0.0.1', + '::1', + '0.0.0.0', + '0:0:0:0:0:0:0:1', + '[::1]', + '[0:0:0:0:0:0:0:1]', +]); + +/** + * Normalize a hostname for comparison + * - Lowercase + * - Remove trailing dots (DNS root) + * - Strip IPv6 brackets + * - Expand common IPv6 localhost representations + * @param {string} hostname + * @returns {string} + */ +function normalizeHostname(hostname) { + if (!hostname) return ''; + let normalized = hostname.toLowerCase().trim(); + + // Remove trailing dot (DNS root indicator) + if (normalized.endsWith('.')) { + normalized = normalized.slice(0, -1); + } + + // Strip IPv6 brackets for comparison + if (normalized.startsWith('[') && normalized.endsWith(']')) { + normalized = normalized.slice(1, -1); + } + + return normalized; +} + +/** + * Check if two hostnames refer to the same host + * Handles localhost aliases, IPv6, and normalization + * @param {string} host1 + * @param {string} host2 + * @returns {boolean} + */ +export function isSameHost(host1, host2) { + const norm1 = normalizeHostname(host1); + const norm2 = normalizeHostname(host2); + + // Direct match after normalization + if (norm1 === norm2) { + return true; + } + + // Check if both are localhost aliases + if (LOCALHOST_ALIASES.has(norm1) && LOCALHOST_ALIASES.has(norm2)) { + return true; + } + + return false; +} + export function setNet(netImpl) { net = netImpl; } @@ -40,7 +101,10 @@ export function initListeners(hsyncClient) { cleanHost = cleanHost.substring(0, cleanHost.length - 1); } const url = new URL(cleanHost); - if (url.hostname.toLowerCase() === hsyncClient.myHostName.toLowerCase()) { + // Security: Use comprehensive hostname comparison to prevent bypass attacks + // CVE-HSYNC-2026-006: Simple string comparison could be bypassed via + // localhost aliases, IPv6, trailing dots, etc. + if (isSameHost(url.hostname, hsyncClient.myHostName)) { throw new Error('targetHost must be a different host'); } debug('creating handler', port, cleanHost); From 1406ac672b31563696462681868de5e1c0bf2ebc Mon Sep 17 00:00:00 2001 From: Luis Montes Date: Fri, 6 Feb 2026 23:57:05 -0700 Subject: [PATCH 2/2] test: add host validation bypass prevention tests --- test/unit/socket-listeners.test.js | 100 ++++++++++++++++++++++++++++- 1 file changed, 99 insertions(+), 1 deletion(-) diff --git a/test/unit/socket-listeners.test.js b/test/unit/socket-listeners.test.js index 4654bfb..1b18dc0 100644 --- a/test/unit/socket-listeners.test.js +++ b/test/unit/socket-listeners.test.js @@ -1,5 +1,49 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { initListeners, setNet } from '../../lib/socket-listeners.js'; +import { initListeners, setNet, isSameHost } from '../../lib/socket-listeners.js'; + +describe('isSameHost', () => { + it('should match identical hostnames', () => { + expect(isSameHost('example.com', 'example.com')).toBe(true); + }); + + it('should be case insensitive', () => { + expect(isSameHost('Example.COM', 'example.com')).toBe(true); + expect(isSameHost('LOCALHOST', 'localhost')).toBe(true); + }); + + it('should handle trailing dots (DNS root)', () => { + expect(isSameHost('example.com.', 'example.com')).toBe(true); + expect(isSameHost('example.com', 'example.com.')).toBe(true); + }); + + it('should match localhost aliases', () => { + expect(isSameHost('localhost', '127.0.0.1')).toBe(true); + expect(isSameHost('localhost', '::1')).toBe(true); + expect(isSameHost('127.0.0.1', '::1')).toBe(true); + expect(isSameHost('localhost', '0.0.0.0')).toBe(true); + }); + + it('should handle IPv6 brackets', () => { + expect(isSameHost('[::1]', '::1')).toBe(true); + expect(isSameHost('[::1]', 'localhost')).toBe(true); + }); + + it('should handle expanded IPv6 localhost', () => { + expect(isSameHost('0:0:0:0:0:0:0:1', '::1')).toBe(true); + expect(isSameHost('0:0:0:0:0:0:0:1', 'localhost')).toBe(true); + }); + + it('should not match different hosts', () => { + expect(isSameHost('example.com', 'other.com')).toBe(false); + expect(isSameHost('localhost', 'example.com')).toBe(false); + }); + + it('should handle empty/null inputs', () => { + expect(isSameHost('', '')).toBe(true); + expect(isSameHost('example.com', '')).toBe(false); + expect(isSameHost('', 'example.com')).toBe(false); + }); +}); describe('socket-listeners', () => { let mockNet; @@ -100,6 +144,60 @@ describe('socket-listeners', () => { ).toThrow('targetHost must be a different host'); }); + it('should throw if targetHost matches client with different case', () => { + expect(() => + listeners.addSocketListener({ + port: 3000, + targetHost: 'https://LOCAL.EXAMPLE.COM', + }) + ).toThrow('targetHost must be a different host'); + }); + + it('should throw if targetHost matches client with trailing dot', () => { + expect(() => + listeners.addSocketListener({ + port: 3000, + targetHost: 'https://local.example.com.', + }) + ).toThrow('targetHost must be a different host'); + }); + + it('should block localhost bypass via 127.0.0.1', () => { + mockHsyncClient.myHostName = 'localhost'; + listeners = initListeners(mockHsyncClient); + + expect(() => + listeners.addSocketListener({ + port: 3000, + targetHost: 'https://127.0.0.1', + }) + ).toThrow('targetHost must be a different host'); + }); + + it('should block localhost bypass via IPv6 ::1', () => { + mockHsyncClient.myHostName = 'localhost'; + listeners = initListeners(mockHsyncClient); + + expect(() => + listeners.addSocketListener({ + port: 3000, + targetHost: 'https://[::1]', + }) + ).toThrow('targetHost must be a different host'); + }); + + it('should block localhost bypass via 0.0.0.0', () => { + mockHsyncClient.myHostName = '127.0.0.1'; + listeners = initListeners(mockHsyncClient); + + expect(() => + listeners.addSocketListener({ + port: 3000, + targetHost: 'https://0.0.0.0', + }) + ).toThrow('targetHost must be a different host'); + }); + it('should clean trailing slash from targetHost', () => { const listener = listeners.addSocketListener({ port: 3000,