Skip to content

Commit

Permalink
Validate Host/Origin headers in InspectorProxyWorker
Browse files Browse the repository at this point in the history
  • Loading branch information
mrbbot committed Dec 5, 2023
1 parent 887afd6 commit ad457f1
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 4 deletions.
8 changes: 8 additions & 0 deletions .changeset/wise-seas-press.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"miniflare": patch
"wrangler": patch
---

fix: validate `Host` and `Orgin` headers where appropriate

`Host` and `Origin` headers are now checked when connecting to the inspector and Miniflare's magic proxy. If these don't match what's expected, the request will fail.
38 changes: 36 additions & 2 deletions fixtures/dev-env/tests/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import assert from "node:assert";
import events from "node:events";
import timers from "node:timers/promises";
import getPort from "get-port";
import {
Miniflare,
Expand Down Expand Up @@ -186,14 +188,17 @@ function fakeReloadComplete(
liveReload: config.dev?.liveReload,
};

setTimeout(() => {
const timeoutPromise = timers.setTimeout(delay).then(() => {
devEnv.proxy.onReloadComplete({
type: "reloadComplete",
config,
bundle: fakeBundle,
proxyData,
});
}, delay);
});
// Add this promise to `fireAndForgetPromises`, ensuring it runs before we
// start the next test
fireAndForgetPromises.push(timeoutPromise);

return { config, mfOpts }; // convenience to allow calling and defining new config/mfOpts inline but also store the new objects
}
Expand Down Expand Up @@ -283,6 +288,35 @@ describe("startDevWorker: ProxyController", () => {
});
});

test("InspectorProxyWorker rejects unauthorised requests", async () => {
const run = await fakeStartUserWorker({
script: `
export default {
fetch() {
return new Response();
}
}
`,
});

// Check validates `Host` header
ws = new WebSocket(
`ws://${run.inspectorProxyWorkerUrl.host}/core:user:${run.config.name}`,
{ setHost: false, headers: { Host: "example.com" } }
);
let openPromise = events.once(ws, "open");
await expect(openPromise).rejects.toThrow("Unexpected server response");

// Check validates `Origin` header
ws = new WebSocket(
`ws://${run.inspectorProxyWorkerUrl.host}/core:user:${run.config.name}`,
{ origin: "https://example.com" }
);
openPromise = events.once(ws, "open");
await expect(openPromise).rejects.toThrow("Unexpected server response");
ws.close();
});

test("User worker exception", async () => {
const consoleErrorSpy = vi.spyOn(console, "error");

Expand Down
6 changes: 4 additions & 2 deletions packages/wrangler/src/api/startDevWorker/ProxyController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ export class ProxyController extends EventEmitter {
workers: [
{
name: "ProxyWorker",
compatibilityFlags: ["nodejs_compat"],
// `url_standard` required to parse IPv6 hostnames correctly
compatibilityFlags: ["nodejs_compat", "url_standard"],
modulesRoot: path.dirname(proxyWorkerPath),
modules: [{ type: "ESModule", path: proxyWorkerPath }],
durableObjects: {
Expand Down Expand Up @@ -96,7 +97,8 @@ export class ProxyController extends EventEmitter {
},
{
name: "InspectorProxyWorker",
compatibilityFlags: ["nodejs_compat"],
// `url_standard` required to parse IPv6 hostnames correctly
compatibilityFlags: ["nodejs_compat", "url_standard"],
modulesRoot: path.dirname(inspectorProxyWorkerPath),
modules: [{ type: "ESModule", path: inspectorProxyWorkerPath }],
durableObjects: {
Expand Down
44 changes: 44 additions & 0 deletions packages/wrangler/templates/startDevWorker/InspectorProxyWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ import {
urlFromParts,
} from "../../src/api/startDevWorker/utils";

const ALLOWED_HOST_HOSTNAMES = ["127.0.0.1", "[::1]", "localhost"];
const ALLOWED_ORIGIN_HOSTNAMES = [
"devtools.devprod.cloudflare.dev",
"cloudflare-devtools.pages.dev",
/^[a-z0-9]+\.cloudflare-devtools\.pages\.dev$/,
"127.0.0.1",
"[::1]",
"localhost",
];

interface Env {
PROXY_CONTROLLER: Fetcher;
PROXY_CONTROLLER_AUTH_SECRET: string;
Expand Down Expand Up @@ -451,6 +461,40 @@ export class InspectorProxyWorker implements DurableObject {
}

async handleDevToolsWebSocketUpgradeRequest(req: Request) {
// Validate `Host` header
let hostHeader = req.headers.get("Host");
if (hostHeader == null) return new Response(null, { status: 400 });
try {
const host = new URL(`http://${hostHeader}`);
if (!ALLOWED_HOST_HOSTNAMES.includes(host.hostname)) {
return new Response("Disallowed `Host` header", { status: 401 });
}
} catch {
return new Response("Expected `Host` header", { status: 400 });
}
// Validate `Origin` header
let originHeader = req.headers.get("Origin");
if (originHeader === null && !req.headers.has("User-Agent")) {
// VSCode doesn't send an `Origin` header, but also doesn't send a
// `User-Agent` header, so allow an empty origin in this case.
originHeader = "http://localhost";
}
if (originHeader === null) {
return new Response("Expected `Origin` header", { status: 400 });
}
try {
const origin = new URL(originHeader);
const allowed = ALLOWED_ORIGIN_HOSTNAMES.some((rule) => {
if (typeof rule === "string") return origin.hostname === rule;
else return rule.test(origin.hostname);
});
if (!allowed) {
return new Response("Disallowed `Origin` header", { status: 401 });
}
} catch {
return new Response("Expected `Origin` header", { status: 400 });
}

// DevTools attempting to connect
this.sendDebugLog("DEVTOOLS WEBSOCKET TRYING TO CONNECT");

Expand Down

0 comments on commit ad457f1

Please sign in to comment.