fix(security): block plaintext WebSocket connections to non-loopback addresses (#20803)
* fix(security): block plaintext WebSocket connections to non-loopback addresses Addresses CWE-319 (Cleartext Transmission of Sensitive Information). Previously, ws:// connections to remote hosts were allowed, exposing both credentials and chat data to network interception. This change blocks ALL plaintext ws:// connections to non-loopback addresses, regardless of whether explicit credentials are configured (device tokens may be loaded dynamically). Security policy: - wss:// allowed to any host - ws:// allowed only to loopback (127.x.x.x, localhost, ::1) - ws:// to LAN/tailnet/remote hosts now requires TLS Changes: - Add isSecureWebSocketUrl() validation in net.ts - Block insecure connections in GatewayClient.start() - Block insecure URLs in buildGatewayConnectionDetails() - Handle malformed URLs gracefully without crashing - Update tests to use wss:// for non-loopback URLs Fixes #12519 * fix(test): update gateway-chat mock to preserve net.js exports Use importOriginal to spread actual module exports and mock only the functions needed for testing. This ensures isSecureWebSocketUrl and other exports remain available to the code under test.
This commit is contained in:
@@ -86,6 +86,77 @@ function getLatestWs(): MockWebSocket {
|
||||
return ws;
|
||||
}
|
||||
|
||||
describe("GatewayClient security checks", () => {
|
||||
beforeEach(() => {
|
||||
wsInstances.length = 0;
|
||||
});
|
||||
|
||||
it("blocks ws:// to non-loopback addresses (CWE-319)", () => {
|
||||
const onConnectError = vi.fn();
|
||||
const client = new GatewayClient({
|
||||
url: "ws://remote.example.com:18789",
|
||||
onConnectError,
|
||||
});
|
||||
|
||||
client.start();
|
||||
|
||||
expect(onConnectError).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: expect.stringContaining("SECURITY ERROR"),
|
||||
}),
|
||||
);
|
||||
expect(wsInstances.length).toBe(0); // No WebSocket created
|
||||
client.stop();
|
||||
});
|
||||
|
||||
it("handles malformed URLs gracefully without crashing", () => {
|
||||
const onConnectError = vi.fn();
|
||||
const client = new GatewayClient({
|
||||
url: "not-a-valid-url",
|
||||
onConnectError,
|
||||
});
|
||||
|
||||
// Should not throw
|
||||
expect(() => client.start()).not.toThrow();
|
||||
|
||||
expect(onConnectError).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: expect.stringContaining("SECURITY ERROR"),
|
||||
}),
|
||||
);
|
||||
expect(wsInstances.length).toBe(0); // No WebSocket created
|
||||
client.stop();
|
||||
});
|
||||
|
||||
it("allows ws:// to loopback addresses", () => {
|
||||
const onConnectError = vi.fn();
|
||||
const client = new GatewayClient({
|
||||
url: "ws://127.0.0.1:18789",
|
||||
onConnectError,
|
||||
});
|
||||
|
||||
client.start();
|
||||
|
||||
expect(onConnectError).not.toHaveBeenCalled();
|
||||
expect(wsInstances.length).toBe(1); // WebSocket created
|
||||
client.stop();
|
||||
});
|
||||
|
||||
it("allows wss:// to any address", () => {
|
||||
const onConnectError = vi.fn();
|
||||
const client = new GatewayClient({
|
||||
url: "wss://remote.example.com:18789",
|
||||
onConnectError,
|
||||
});
|
||||
|
||||
client.start();
|
||||
|
||||
expect(onConnectError).not.toHaveBeenCalled();
|
||||
expect(wsInstances.length).toBe(1); // WebSocket created
|
||||
client.stop();
|
||||
});
|
||||
});
|
||||
|
||||
describe("GatewayClient close handling", () => {
|
||||
beforeEach(() => {
|
||||
wsInstances.length = 0;
|
||||
|
||||
Reference in New Issue
Block a user