Skip to content

Commit 49a4696

Browse files
mrbbotjspspike
andauthored
[wrangler] Backport recent inspector proxy server changes to V2 (#4587)
* Change dev registry and inspector server to use 127.0.0.1 instead of all interfaces (#4437) (cherry picked from commit 05b1bbd) * fix: validate `Host`/`Origin` headers in inspector proxy Backport of #4550 --------- Co-authored-by: Joshua Johnson <[email protected]>
1 parent 84f4276 commit 49a4696

File tree

5 files changed

+69
-12
lines changed

5 files changed

+69
-12
lines changed

.changeset/warm-dryers-double.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"wrangler": patch
3+
---
4+
5+
Change dev registry and inspector server to listen on 127.0.0.1 instead of all interfaces

.changeset/wise-seas-press.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"wrangler": patch
3+
---
4+
5+
fix: validate `Host` and `Orgin` headers where appropriate
6+
7+
`Host` and `Origin` headers are now checked when connecting to the inspector proxy. If these don't match what's expected, the request will fail.

packages/wrangler/src/__tests__/api-devregistry.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ describe("multi-worker testing", () => {
4040
});
4141

4242
it("parentWorker and childWorker should be added devRegistry", async () => {
43-
const resp = await fetch("http://localhost:6284/workers");
43+
const resp = await fetch("http://127.0.0.1:6284/workers");
4444
if (resp) {
4545
const parsedResp = (await resp.json()) as {
4646
parent: unknown;

packages/wrangler/src/dev-registry.ts

+7-7
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
1-
import http from "http";
21
import net from "net";
2+
import { createServer } from "node:http";
33
import bodyParser from "body-parser";
44
import express from "express";
55
import { createHttpTerminator } from "http-terminator";
66
import { fetch } from "undici";
77
import { logger } from "./logger";
88

99
import type { Config } from "./config";
10-
import type { Server } from "http";
1110
import type { HttpTerminator } from "http-terminator";
11+
import type { Server } from "node:http";
1212

13-
const DEV_REGISTRY_PORT = "6284";
14-
const DEV_REGISTRY_HOST = `http://localhost:${DEV_REGISTRY_PORT}`;
13+
const DEV_REGISTRY_PORT = 6284;
14+
const DEV_REGISTRY_HOST = `http://127.0.0.1:${DEV_REGISTRY_PORT}`;
1515

1616
let server: Server | null;
1717
let terminator: HttpTerminator;
@@ -48,7 +48,7 @@ async function isPortAvailable() {
4848
netServer.close();
4949
resolve(true);
5050
});
51-
netServer.listen(DEV_REGISTRY_PORT);
51+
netServer.listen(DEV_REGISTRY_PORT, "127.0.0.1");
5252
});
5353
}
5454

@@ -80,9 +80,9 @@ export async function startWorkerRegistry() {
8080
workers = {};
8181
res.json(null);
8282
});
83-
server = http.createServer(app);
83+
server = createServer(app);
8484
terminator = createHttpTerminator({ server });
85-
server.listen(DEV_REGISTRY_PORT);
85+
server.listen(DEV_REGISTRY_PORT, "127.0.0.1");
8686

8787
/**
8888
* The registry server may have already been started by another wrangler process.

packages/wrangler/src/inspect.ts

+49-4
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,46 @@ interface InspectorProps {
7575
name?: string;
7676
}
7777

78+
const ALLOWED_HOST_HOSTNAMES = ["127.0.0.1", "[::1]", "localhost"];
79+
const ALLOWED_ORIGIN_HOSTNAMES = [
80+
"devtools.devprod.cloudflare.dev",
81+
"cloudflare-devtools.pages.dev",
82+
/^[a-z0-9]+\.cloudflare-devtools\.pages\.dev$/,
83+
"127.0.0.1",
84+
"[::1]",
85+
"localhost",
86+
];
87+
function validateWebSocketHandshake(req: IncomingMessage) {
88+
// Validate `Host` header
89+
const hostHeader = req.headers.host;
90+
if (hostHeader == null) return false;
91+
try {
92+
const host = new URL(`http://${hostHeader}`);
93+
if (!ALLOWED_HOST_HOSTNAMES.includes(host.hostname)) return false;
94+
} catch {
95+
return false;
96+
}
97+
// Validate `Origin` header
98+
let originHeader = req.headers.origin;
99+
if (originHeader === null && req.headers["user-agent"] === undefined) {
100+
// VSCode doesn't send an `Origin` header, but also doesn't send a
101+
// `User-Agent` header, so allow an empty origin in this case.
102+
originHeader = "http://localhost";
103+
}
104+
if (originHeader == null) return false;
105+
try {
106+
const origin = new URL(originHeader);
107+
const allowed = ALLOWED_ORIGIN_HOSTNAMES.some((rule) => {
108+
if (typeof rule === "string") return origin.hostname === rule;
109+
else return rule.test(origin.hostname);
110+
});
111+
if (!allowed) return false;
112+
} catch {
113+
return false;
114+
}
115+
return true;
116+
}
117+
78118
export default function useInspector(props: InspectorProps) {
79119
/** A unique ID for this session. */
80120
const inspectorIdRef = useRef(randomId());
@@ -110,7 +150,7 @@ export default function useInspector(props: InspectorProps) {
110150
case "/json/list":
111151
{
112152
res.setHeader("Content-Type", "application/json");
113-
const localHost = `localhost:${props.port}/ws`;
153+
const localHost = `127.0.0.1:${props.port}/ws`;
114154
const devtoolsFrontendUrl = `devtools://devtools/bundled/js_app.html?experiments=true&v8only=true&ws=${localHost}`;
115155
const devtoolsFrontendUrlCompat = `devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=${localHost}`;
116156
res.end(
@@ -155,7 +195,12 @@ export default function useInspector(props: InspectorProps) {
155195
}
156196
const wsServer = wsServerRef.current;
157197

158-
wsServer.on("connection", (ws: WebSocket) => {
198+
wsServer.on("connection", (ws: WebSocket, req: IncomingMessage) => {
199+
if (!validateWebSocketHandshake(req)) {
200+
ws.close(1008, "Unauthorised");
201+
return;
202+
}
203+
159204
if (wsServer.clients.size > 1) {
160205
/** We only want to have one active Devtools instance at a time. */
161206
logger.error(
@@ -197,7 +242,7 @@ export default function useInspector(props: InspectorProps) {
197242
timeout: 2000,
198243
abortSignal: abortController.signal,
199244
});
200-
server.listen(props.port);
245+
server.listen(props.port, "127.0.0.1");
201246
}
202247
startInspectorProxy().catch((err) => {
203248
if ((err as { code: string }).code !== "ABORT_ERR") {
@@ -844,7 +889,7 @@ export const openInspector = async (
844889
) => {
845890
const query = new URLSearchParams();
846891
query.set("theme", "systemPreferred");
847-
query.set("ws", `localhost:${inspectorPort}/ws`);
892+
query.set("ws", `127.0.0.1:${inspectorPort}/ws`);
848893
if (worker) query.set("domain", worker);
849894
const url = `https://devtools.devprod.cloudflare.dev/js_app?${query.toString()}`;
850895
const errorMessage =

0 commit comments

Comments
 (0)