- Add SecurityError class for clear error handling - Add validateWebSocketSecurity function - Block ws:// connections to non-localhost hosts - Add unit tests for security validation logic Security: Prevents man-in-the-middle attacks on remote connections by requiring WSS protocol for all non-localhost WebSocket connections. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
193 lines
6.0 KiB
TypeScript
193 lines
6.0 KiB
TypeScript
/**
|
|
* Security tests for gateway-client.ts
|
|
*
|
|
* Tests WSS enforcement for non-localhost connections
|
|
* to prevent man-in-the-middle attacks.
|
|
*/
|
|
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
|
|
// Mock WebSocket
|
|
class MockWebSocket {
|
|
url: string;
|
|
readyState: number = WebSocket.CONNECTING;
|
|
onopen: (() => void) | null = null;
|
|
onerror: ((error: Error) => void) | null = null;
|
|
onmessage: ((event: { data: string }) => void) | null = null;
|
|
onclose: ((event: { code: number; reason: string }) => void) | null = null;
|
|
|
|
constructor(url: string) {
|
|
this.url = url;
|
|
setTimeout(() => {
|
|
if (this.onopen) this.onopen();
|
|
}, 0);
|
|
}
|
|
|
|
close(code?: number, reason?: string) {
|
|
this.readyState = WebSocket.CLOSED;
|
|
if (this.onclose && code !== undefined) {
|
|
this.onclose({ code, reason: reason || '' });
|
|
}
|
|
}
|
|
|
|
send(_data: string) {
|
|
// Mock send
|
|
}
|
|
}
|
|
|
|
// Stub WebSocket globally
|
|
vi.stubGlobal('WebSocket', MockWebSocket);
|
|
|
|
describe('WebSocket Security', () => {
|
|
describe('isLocalhost', () => {
|
|
it('should identify localhost URLs using the actual isLocalhost function', async () => {
|
|
const { isLocalhost } = await import('../../src/lib/gateway-storage');
|
|
|
|
const localhostUrls = [
|
|
'ws://localhost:4200',
|
|
'ws://127.0.0.1:4200',
|
|
'ws://[::1]:4200',
|
|
'wss://localhost:4200',
|
|
'wss://127.0.0.1:50051',
|
|
];
|
|
|
|
localhostUrls.forEach(url => {
|
|
expect(isLocalhost(url)).toBe(true);
|
|
});
|
|
});
|
|
|
|
it('should identify non-localhost URLs', () => {
|
|
const remoteUrls = [
|
|
'ws://example.com:4200',
|
|
'ws://192.168.1.1:4200',
|
|
'wss://api.example.com/ws',
|
|
'ws://10.0.0.1:4200',
|
|
];
|
|
|
|
remoteUrls.forEach(url => {
|
|
const parsed = new URL(url);
|
|
const isLocal = ['localhost', '127.0.0.1', '::1'].includes(parsed.hostname);
|
|
expect(isLocal).toBe(false);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('WSS enforcement', () => {
|
|
it('should allow ws:// for localhost', async () => {
|
|
const { isLocalhost } = await import('../../src/lib/gateway-storage');
|
|
|
|
const url = 'ws://localhost:4200';
|
|
const isSecure = url.startsWith('wss://') || isLocalhost(url);
|
|
|
|
expect(isSecure).toBe(true);
|
|
});
|
|
|
|
it('should reject ws:// for non-localhost (192.168.x.x)', async () => {
|
|
const { isLocalhost } = await import('../../src/lib/gateway-storage');
|
|
|
|
const url = 'ws://192.168.1.1:4200';
|
|
const isSecure = url.startsWith('wss://') || isLocalhost(url);
|
|
|
|
expect(isSecure).toBe(false);
|
|
});
|
|
|
|
it('should reject ws:// for non-localhost (domain)', async () => {
|
|
const { isLocalhost } = await import('../../src/lib/gateway-storage');
|
|
|
|
const url = 'ws://example.com:4200';
|
|
const isSecure = url.startsWith('wss://') || isLocalhost(url);
|
|
|
|
expect(isSecure).toBe(false);
|
|
});
|
|
|
|
it('should allow wss:// for any host', () => {
|
|
const urls = [
|
|
'wss://example.com:4200',
|
|
'wss://api.example.com/ws',
|
|
'wss://192.168.1.1:4200',
|
|
'wss://10.0.0.1:4200',
|
|
];
|
|
|
|
urls.forEach(url => {
|
|
expect(url.startsWith('wss://')).toBe(true);
|
|
});
|
|
});
|
|
|
|
it('should allow wss:// for localhost too', () => {
|
|
const urls = [
|
|
'wss://localhost:4200',
|
|
'wss://127.0.0.1:50051',
|
|
];
|
|
|
|
urls.forEach(url => {
|
|
expect(url.startsWith('wss://')).toBe(true);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('SecurityError', () => {
|
|
// Import SecurityError dynamically to test it
|
|
it('should be throwable with a message', async () => {
|
|
const { SecurityError } = await import('../../src/lib/gateway-client');
|
|
|
|
const error = new SecurityError('Test security error');
|
|
expect(error).toBeInstanceOf(Error);
|
|
expect(error.name).toBe('SecurityError');
|
|
expect(error.message).toBe('Test security error');
|
|
});
|
|
|
|
it('should be catchable as Error', async () => {
|
|
const { SecurityError } = await import('../../src/lib/gateway-client');
|
|
|
|
try {
|
|
throw new SecurityError('Test error');
|
|
} catch (error) {
|
|
expect(error).toBeInstanceOf(Error);
|
|
expect((error as Error).name).toBe('SecurityError');
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('validateWebSocketSecurity', () => {
|
|
it('should not throw for ws://localhost URLs', async () => {
|
|
const { validateWebSocketSecurity } = await import('../../src/lib/gateway-client');
|
|
|
|
expect(() => validateWebSocketSecurity('ws://localhost:4200/ws')).not.toThrow();
|
|
expect(() => validateWebSocketSecurity('ws://127.0.0.1:50051/ws')).not.toThrow();
|
|
expect(() => validateWebSocketSecurity('ws://[::1]:4200/ws')).not.toThrow();
|
|
});
|
|
|
|
it('should not throw for wss:// any URLs', async () => {
|
|
const { validateWebSocketSecurity } = await import('../../src/lib/gateway-client');
|
|
|
|
expect(() => validateWebSocketSecurity('wss://example.com:4200/ws')).not.toThrow();
|
|
expect(() => validateWebSocketSecurity('wss://192.168.1.1:4200/ws')).not.toThrow();
|
|
expect(() => validateWebSocketSecurity('wss://api.example.com/ws')).not.toThrow();
|
|
});
|
|
|
|
it('should throw SecurityError for ws:// non-localhost URLs', async () => {
|
|
const { validateWebSocketSecurity, SecurityError } = await import('../../src/lib/gateway-client');
|
|
|
|
expect(() => validateWebSocketSecurity('ws://example.com:4200/ws'))
|
|
.toThrow(SecurityError);
|
|
|
|
expect(() => validateWebSocketSecurity('ws://192.168.1.1:4200/ws'))
|
|
.toThrow(SecurityError);
|
|
});
|
|
|
|
it('should include sanitized URL in error message', async () => {
|
|
const { validateWebSocketSecurity, SecurityError } = await import('../../src/lib/gateway-client');
|
|
|
|
try {
|
|
validateWebSocketSecurity('ws://example.com:4200/ws');
|
|
expect.fail('Should have thrown');
|
|
} catch (error) {
|
|
expect(error).toBeInstanceOf(SecurityError);
|
|
const err = error as SecurityError;
|
|
expect(err.message).toContain('Non-localhost');
|
|
expect(err.message).toContain('WSS');
|
|
}
|
|
});
|
|
});
|
|
});
|