diff --git a/src/web-server/middleware/auth-middleware.ts b/src/web-server/middleware/auth-middleware.ts index 39228916..204b4c97 100644 --- a/src/web-server/middleware/auth-middleware.ts +++ b/src/web-server/middleware/auth-middleware.ts @@ -9,6 +9,7 @@ import session from 'express-session'; import rateLimit from 'express-rate-limit'; import crypto from 'crypto'; +import * as net from 'net'; import fs from 'fs'; import path from 'path'; import { @@ -161,7 +162,7 @@ function isLoopbackHostname(value: string | undefined): boolean { return ( normalized === 'localhost' || normalized.endsWith('.localhost') || - isLoopbackRemoteAddress(normalized) + (net.isIP(normalized) !== 0 && isLoopbackRemoteAddress(normalized)) ); } diff --git a/tests/unit/web-server/auth-middleware.test.ts b/tests/unit/web-server/auth-middleware.test.ts index a252cda2..fabe6583 100644 --- a/tests/unit/web-server/auth-middleware.test.ts +++ b/tests/unit/web-server/auth-middleware.test.ts @@ -239,6 +239,18 @@ describe('Dashboard Auth', () => { expect(isDashboardWebSocketUpgradeAllowed(request)).toBe(true); }); + it('blocks 127-prefixed DNS names from loopback websocket origin aliases', () => { + process.env.CCS_DASHBOARD_AUTH_ENABLED = 'false'; + const request = makeUpgradeRequest('127.0.0.1', false, { + host: 'localhost:3001', + origin: 'http://127.evil.example.test:3001', + }); + + expect(isDashboardWebSocketOriginAllowed(request)).toBe(false); + expect(isDashboardWebSocketUpgradeAllowed(request)).toBe(false); + expect(getDashboardWebSocketRejectionStatus(request)).toBe(403); + }); + it('blocks cross-site websocket origins even with an authenticated session', () => { process.env.CCS_DASHBOARD_AUTH_ENABLED = 'true'; const request = makeUpgradeRequest('127.0.0.1', true, {