diff --git a/packages/dev-middleware/src/__tests__/InspectorDebuggerUtils.js b/packages/dev-middleware/src/__tests__/InspectorDebuggerUtils.js index 172d0270a798a9..d847bce6aa7b7d 100644 --- a/packages/dev-middleware/src/__tests__/InspectorDebuggerUtils.js +++ b/packages/dev-middleware/src/__tests__/InspectorDebuggerUtils.js @@ -22,11 +22,17 @@ export class DebuggerAgent { #ws: ?WebSocket; #readyPromise: Promise; - constructor(url: string, signal?: AbortSignal, hostHeader?: ?string) { + constructor( + url: string, + signal?: AbortSignal, + headers?: ?{[string]: unknown}, + ) { const ws = new WebSocket(url, { // The mock server uses a self-signed certificate. rejectUnauthorized: false, - ...(hostHeader != null ? {headers: {Host: hostHeader}} : {}), + ...(headers != null + ? {headers} + : {headers: {Origin: 'http://localhost:8081'}}), }); this.#ws = ws; ws.on('message', data => { @@ -116,9 +122,9 @@ export class DebuggerMock extends DebuggerAgent { export async function createDebuggerMock( url: string, signal: AbortSignal, - hostHeader?: ?string, + headers?: ?{[string]: unknown}, ): Promise { - const debuggerMock = new DebuggerMock(url, signal, hostHeader); + const debuggerMock = new DebuggerMock(url, signal, headers); await debuggerMock.ready(); return debuggerMock; } diff --git a/packages/dev-middleware/src/__tests__/InspectorProtocolUtils.js b/packages/dev-middleware/src/__tests__/InspectorProtocolUtils.js index 3d12d534541243..3b7470bbc02f2f 100644 --- a/packages/dev-middleware/src/__tests__/InspectorProtocolUtils.js +++ b/packages/dev-middleware/src/__tests__/InspectorProtocolUtils.js @@ -116,11 +116,12 @@ export async function createAndConnectTarget( signal: AbortSignal, page: PageFromDevice, { - debuggerHostHeader = null, + debuggerHeaders = null, deviceId = null, deviceHostHeader = null, }: Readonly<{ - debuggerHostHeader?: ?string, + debuggerHeaders?: ?{[string]: unknown}, + debuggerOriginHeader?: ?string, deviceId?: ?string, deviceHostHeader?: ?string, }> = {}, @@ -151,7 +152,7 @@ export async function createAndConnectTarget( debugger_ = await createDebuggerMock( webSocketDebuggerUrl, signal, - debuggerHostHeader, + debuggerHeaders, ); await until(() => expect(device.connect).toBeCalled()); } catch (e) { diff --git a/packages/dev-middleware/src/__tests__/InspectorProxyCdpRewritingHacks-test.js b/packages/dev-middleware/src/__tests__/InspectorProxyCdpRewritingHacks-test.js index 3b5513f314c332..7be509389c4bcb 100644 --- a/packages/dev-middleware/src/__tests__/InspectorProxyCdpRewritingHacks-test.js +++ b/packages/dev-middleware/src/__tests__/InspectorProxyCdpRewritingHacks-test.js @@ -273,7 +273,10 @@ describe.each(['HTTP', 'HTTPS'])( vm: 'bar-vm', }, { - debuggerHostHeader: 'localhost:' + serverRef.port, + debuggerHeaders: { + Host: 'localhost:' + serverRef.port, + Origin: 'http://localhost:8081', + }, deviceHostHeader: sourceHost, }, ); diff --git a/packages/dev-middleware/src/__tests__/InspectorProxyCdpTransport-test.js b/packages/dev-middleware/src/__tests__/InspectorProxyCdpTransport-test.js index 7b49a560beb493..39bdf1503f4a10 100644 --- a/packages/dev-middleware/src/__tests__/InspectorProxyCdpTransport-test.js +++ b/packages/dev-middleware/src/__tests__/InspectorProxyCdpTransport-test.js @@ -384,5 +384,41 @@ describe.each(['HTTP', 'HTTPS'])( debugger2?.close(); } }); + + test('debugger connection with invalid origin is rejected', async () => { + const device1 = await createDeviceMock( + `${serverRef.serverBaseWsUrl}/inspector/device?device=device1&name=foo&app=bar`, + autoCleanup.signal, + ); + try { + device1.getPages.mockImplementation(() => [ + { + app: 'bar-app', + id: 'page1', + title: 'bar-title', + vm: 'bar-vm', + }, + ]); + + let pageList: JsonPagesListResponse = []; + await until(async () => { + pageList = (await fetchJson( + `${serverRef.serverBaseUrl}/json`, + // $FlowFixMe[unclear-type] + ): any); + expect(pageList).toHaveLength(1); + }); + const [{webSocketDebuggerUrl}] = pageList; + expect(webSocketDebuggerUrl).toBeDefined(); + + await expect( + createDebuggerMock(webSocketDebuggerUrl, autoCleanup.signal, { + Origin: 'null', + }), + ).rejects.toThrow('Unexpected server response: 401'); + } finally { + device1.close(); + } + }); }, ); diff --git a/packages/dev-middleware/src/inspector-proxy/InspectorProxy.js b/packages/dev-middleware/src/inspector-proxy/InspectorProxy.js index 19862b95d87f47..ea887884d31966 100644 --- a/packages/dev-middleware/src/inspector-proxy/InspectorProxy.js +++ b/packages/dev-middleware/src/inspector-proxy/InspectorProxy.js @@ -32,6 +32,11 @@ import WS from 'ws'; const debug = require('debug')('Metro:InspectorProxy'); +const WS_DEBUGGER_ALLOWED_ORIGINS = new Set([ + 'http://localhost:8081', + 'http://127.0.0.1:8081', +]); + const WS_DEVICE_URL = '/inspector/device'; const WS_DEBUGGER_URL = '/inspector/debug'; const PAGES_LIST_JSON_URL = '/json'; @@ -484,7 +489,17 @@ export default class InspectorProxy implements InspectorProxyQueries { // Don't crash on exceptionally large messages - assume the debugger is // well-behaved and the device is prepared to handle large messages. maxPayload: 0, + // Verify the client is from an allowed origin. + // $FlowFixMe[incompatible-type] - `ws` definition is incomplete. + verifyClient: ( + info: Readonly<{ + origin: string, + secure: boolean, + req: http$IncomingMessage<>, + }>, + ) => WS_DEBUGGER_ALLOWED_ORIGINS.has(info.origin), }); + // $FlowFixMe[value-as-type] wss.on('connection', async (socket: WS, req) => { const wssTimestamp = Date.now();