Security: Reject cross-origin connections to daemon and stream server (#274)
This commit is contained in:
@@ -3,6 +3,42 @@ import * as os from 'os';
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { getSocketDir } from './daemon.js';
|
import { getSocketDir } from './daemon.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request detection pattern used in daemon.ts to prevent cross-origin attacks.
|
||||||
|
* This pattern detects HTTP method prefixes that browsers must send when using fetch().
|
||||||
|
*/
|
||||||
|
const HTTP_REQUEST_PATTERN = /^(GET|POST|PUT|DELETE|HEAD|OPTIONS|PATCH|CONNECT|TRACE)\s/i;
|
||||||
|
|
||||||
|
describe('HTTP request detection (security)', () => {
|
||||||
|
it('should detect POST requests from fetch()', () => {
|
||||||
|
const httpRequest = 'POST / HTTP/1.1\r\nHost: 127.0.0.1:51234\r\n';
|
||||||
|
expect(HTTP_REQUEST_PATTERN.test(httpRequest.trimStart())).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect GET requests', () => {
|
||||||
|
expect(HTTP_REQUEST_PATTERN.test('GET / HTTP/1.1')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect OPTIONS preflight requests', () => {
|
||||||
|
expect(HTTP_REQUEST_PATTERN.test('OPTIONS / HTTP/1.1')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should NOT detect valid JSON commands', () => {
|
||||||
|
const jsonCommand = '{"id":"1","action":"navigate","url":"https://example.com"}';
|
||||||
|
expect(HTTP_REQUEST_PATTERN.test(jsonCommand.trimStart())).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should NOT detect JSON with leading whitespace', () => {
|
||||||
|
const jsonCommand = ' {"id":"1","action":"click","selector":"button"}';
|
||||||
|
expect(HTTP_REQUEST_PATTERN.test(jsonCommand.trimStart())).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be case insensitive for HTTP methods', () => {
|
||||||
|
expect(HTTP_REQUEST_PATTERN.test('post / HTTP/1.1')).toBe(true);
|
||||||
|
expect(HTTP_REQUEST_PATTERN.test('Post / HTTP/1.1')).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('getSocketDir', () => {
|
describe('getSocketDir', () => {
|
||||||
const originalEnv = { ...process.env };
|
const originalEnv = { ...process.env };
|
||||||
|
|
||||||
|
|||||||
@@ -196,10 +196,23 @@ export async function startDaemon(options?: { streamPort?: number }): Promise<vo
|
|||||||
|
|
||||||
const server = net.createServer((socket) => {
|
const server = net.createServer((socket) => {
|
||||||
let buffer = '';
|
let buffer = '';
|
||||||
|
let httpChecked = false;
|
||||||
|
|
||||||
socket.on('data', async (data) => {
|
socket.on('data', async (data) => {
|
||||||
buffer += data.toString();
|
buffer += data.toString();
|
||||||
|
|
||||||
|
// Security: Detect and reject HTTP requests to prevent cross-origin attacks.
|
||||||
|
// Browsers using fetch() must send HTTP headers (e.g., "POST / HTTP/1.1"),
|
||||||
|
// while legitimate clients send raw JSON starting with "{".
|
||||||
|
if (!httpChecked) {
|
||||||
|
httpChecked = true;
|
||||||
|
const trimmed = buffer.trimStart();
|
||||||
|
if (/^(GET|POST|PUT|DELETE|HEAD|OPTIONS|PATCH|CONNECT|TRACE)\s/i.test(trimmed)) {
|
||||||
|
socket.destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Process complete lines
|
// Process complete lines
|
||||||
while (buffer.includes('\n')) {
|
while (buffer.includes('\n')) {
|
||||||
const newlineIdx = buffer.indexOf('\n');
|
const newlineIdx = buffer.indexOf('\n');
|
||||||
|
|||||||
+19
-1
@@ -87,7 +87,25 @@ export class StreamServer {
|
|||||||
start(): Promise<void> {
|
start(): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
this.wss = new WebSocketServer({ port: this.port });
|
this.wss = new WebSocketServer({
|
||||||
|
port: this.port,
|
||||||
|
// Security: Reject cross-origin WebSocket connections from browsers.
|
||||||
|
// This prevents malicious web pages from connecting and injecting input events.
|
||||||
|
verifyClient: (info: {
|
||||||
|
origin: string;
|
||||||
|
secure: boolean;
|
||||||
|
req: import('http').IncomingMessage;
|
||||||
|
}) => {
|
||||||
|
const origin = info.origin;
|
||||||
|
// Allow connections with no origin (non-browser clients like CLI tools)
|
||||||
|
// Reject connections from web pages (which always have an origin)
|
||||||
|
if (origin && !origin.startsWith('file://')) {
|
||||||
|
console.log(`[StreamServer] Rejected connection from origin: ${origin}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
this.wss.on('connection', (ws) => {
|
this.wss.on('connection', (ws) => {
|
||||||
this.handleConnection(ws);
|
this.handleConnection(ws);
|
||||||
|
|||||||
Reference in New Issue
Block a user