Skip to content

Commit f862e2f

Browse files
authored
Security: Reject cross-origin connections to daemon and stream server (#274)
1 parent fcee8f7 commit f862e2f

3 files changed

Lines changed: 68 additions & 1 deletion

File tree

src/daemon.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,42 @@ import * as os from 'os';
33
import * as path from 'path';
44
import { getSocketDir } from './daemon.js';
55

6+
/**
7+
* HTTP request detection pattern used in daemon.ts to prevent cross-origin attacks.
8+
* This pattern detects HTTP method prefixes that browsers must send when using fetch().
9+
*/
10+
const HTTP_REQUEST_PATTERN = /^(GET|POST|PUT|DELETE|HEAD|OPTIONS|PATCH|CONNECT|TRACE)\s/i;
11+
12+
describe('HTTP request detection (security)', () => {
13+
it('should detect POST requests from fetch()', () => {
14+
const httpRequest = 'POST / HTTP/1.1\r\nHost: 127.0.0.1:51234\r\n';
15+
expect(HTTP_REQUEST_PATTERN.test(httpRequest.trimStart())).toBe(true);
16+
});
17+
18+
it('should detect GET requests', () => {
19+
expect(HTTP_REQUEST_PATTERN.test('GET / HTTP/1.1')).toBe(true);
20+
});
21+
22+
it('should detect OPTIONS preflight requests', () => {
23+
expect(HTTP_REQUEST_PATTERN.test('OPTIONS / HTTP/1.1')).toBe(true);
24+
});
25+
26+
it('should NOT detect valid JSON commands', () => {
27+
const jsonCommand = '{"id":"1","action":"navigate","url":"https://example.com"}';
28+
expect(HTTP_REQUEST_PATTERN.test(jsonCommand.trimStart())).toBe(false);
29+
});
30+
31+
it('should NOT detect JSON with leading whitespace', () => {
32+
const jsonCommand = ' {"id":"1","action":"click","selector":"button"}';
33+
expect(HTTP_REQUEST_PATTERN.test(jsonCommand.trimStart())).toBe(false);
34+
});
35+
36+
it('should be case insensitive for HTTP methods', () => {
37+
expect(HTTP_REQUEST_PATTERN.test('post / HTTP/1.1')).toBe(true);
38+
expect(HTTP_REQUEST_PATTERN.test('Post / HTTP/1.1')).toBe(true);
39+
});
40+
});
41+
642
describe('getSocketDir', () => {
743
const originalEnv = { ...process.env };
844

src/daemon.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,10 +196,23 @@ export async function startDaemon(options?: { streamPort?: number }): Promise<vo
196196

197197
const server = net.createServer((socket) => {
198198
let buffer = '';
199+
let httpChecked = false;
199200

200201
socket.on('data', async (data) => {
201202
buffer += data.toString();
202203

204+
// Security: Detect and reject HTTP requests to prevent cross-origin attacks.
205+
// Browsers using fetch() must send HTTP headers (e.g., "POST / HTTP/1.1"),
206+
// while legitimate clients send raw JSON starting with "{".
207+
if (!httpChecked) {
208+
httpChecked = true;
209+
const trimmed = buffer.trimStart();
210+
if (/^(GET|POST|PUT|DELETE|HEAD|OPTIONS|PATCH|CONNECT|TRACE)\s/i.test(trimmed)) {
211+
socket.destroy();
212+
return;
213+
}
214+
}
215+
203216
// Process complete lines
204217
while (buffer.includes('\n')) {
205218
const newlineIdx = buffer.indexOf('\n');

src/stream-server.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,25 @@ export class StreamServer {
8787
start(): Promise<void> {
8888
return new Promise((resolve, reject) => {
8989
try {
90-
this.wss = new WebSocketServer({ port: this.port });
90+
this.wss = new WebSocketServer({
91+
port: this.port,
92+
// Security: Reject cross-origin WebSocket connections from browsers.
93+
// This prevents malicious web pages from connecting and injecting input events.
94+
verifyClient: (info: {
95+
origin: string;
96+
secure: boolean;
97+
req: import('http').IncomingMessage;
98+
}) => {
99+
const origin = info.origin;
100+
// Allow connections with no origin (non-browser clients like CLI tools)
101+
// Reject connections from web pages (which always have an origin)
102+
if (origin && !origin.startsWith('file://')) {
103+
console.log(`[StreamServer] Rejected connection from origin: ${origin}`);
104+
return false;
105+
}
106+
return true;
107+
},
108+
});
91109

92110
this.wss.on('connection', (ws) => {
93111
this.handleConnection(ws);

0 commit comments

Comments
 (0)