Skip to content

Commit

Permalink
[wrangler] fix: change wrangler (pages) dev to listen on `localhost…
Browse files Browse the repository at this point in the history
…` by default (#4532)

* fix: change `wrangler (pages) dev` to listen on `localhost` by default

* fix: disable `MiniflareDurableObject` endpoints by default

* fix: prevent unauthorised access to proxy server

Require `MF-Op-Secret` header to match `MINIFLARE_PROXY_SECRET` data
binding before request processed.
  • Loading branch information
mrbbot authored Nov 30, 2023
1 parent 310281a commit 311ffbd
Show file tree
Hide file tree
Showing 20 changed files with 108 additions and 11 deletions.
7 changes: 7 additions & 0 deletions .changeset/shiny-poets-smell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"wrangler": minor
---

fix: change `wrangler (pages) dev` to listen on `localhost` by default

Previously, Wrangler listened on all interfaces (`*`) by default. This change switches `wrangler (pages) dev` to just listen on local interfaces. Whilst this is technically a breaking change, we've decided the security benefits outweigh the potential disruption caused. If you need to access your dev server from another device on your network, you can use `wrangler (pages) dev --ip *` to restore the previous behaviour.
2 changes: 2 additions & 0 deletions packages/miniflare/src/plugins/cache/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
PersistenceSchema,
Plugin,
SERVICE_LOOPBACK,
getControlEndpointBindings,
getPersistPath,
} from "../shared";

Expand Down Expand Up @@ -128,6 +129,7 @@ export const CACHE_PLUGIN: Plugin<
name: SharedBindings.MAYBE_SERVICE_LOOPBACK,
service: { name: SERVICE_LOOPBACK },
},
...getControlEndpointBindings(),
],
},
};
Expand Down
5 changes: 5 additions & 0 deletions packages/miniflare/src/plugins/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import {
convertModuleDefinition,
withSourceURL,
} from "./modules";
import { PROXY_SECRET } from "./proxy";
import { ServiceDesignatorSchema } from "./services";

// `workerd`'s `trustBrowserCas` should probably be named `trustSystemCas`.
Expand Down Expand Up @@ -637,6 +638,10 @@ export function getGlobalServices({
name: CoreBindings.DURABLE_OBJECT_NAMESPACE_PROXY,
durableObjectNamespace: { className: "ProxyServer" },
},
{
name: CoreBindings.DATA_PROXY_SECRET,
data: PROXY_SECRET,
},
// Add `proxyBindings` here, they'll be added to the `ProxyServer` `env`
...proxyBindings,
];
Expand Down
13 changes: 13 additions & 0 deletions packages/miniflare/src/plugins/core/proxy/client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/ban-types */
import assert from "assert";
import crypto from "crypto";
import { ReadableStream, TransformStream } from "stream/web";
import util from "util";
import type { ServiceWorkerGlobalScope } from "@cloudflare/workers-types/experimental";
Expand Down Expand Up @@ -63,6 +64,9 @@ const revivers: ReducersRevivers = {
// `Native` reviver depends on `ProxyStubHandler` methods
};

export const PROXY_SECRET = crypto.randomBytes(16);
const PROXY_SECRET_HEX = PROXY_SECRET.toString("hex");

// Exported public API of the proxy system
export class ProxyClient {
#bridge: ProxyClientBridge;
Expand Down Expand Up @@ -168,6 +172,7 @@ class ProxyClientBridge {
await this.dispatchFetch(this.url, {
method: "DELETE",
headers: {
[CoreHeaders.OP_SECRET]: PROXY_SECRET_HEX,
[CoreHeaders.OP]: ProxyOps.FREE,
[CoreHeaders.OP_TARGET]: addresses.join(","),
},
Expand Down Expand Up @@ -234,6 +239,7 @@ class ProxyStubHandler<T extends object> implements ProxyHandler<T> {
const resPromise = this.bridge.dispatchFetch(this.bridge.url, {
method: "POST",
headers: {
[CoreHeaders.OP_SECRET]: PROXY_SECRET_HEX,
[CoreHeaders.OP]: ProxyOps.GET, // GET without key just gets target
[CoreHeaders.OP_TARGET]: stringify(target, reducers),
},
Expand Down Expand Up @@ -367,6 +373,7 @@ class ProxyStubHandler<T extends object> implements ProxyHandler<T> {
const syncRes = this.bridge.sync.fetch(this.bridge.url, {
method: "POST",
headers: {
[CoreHeaders.OP_SECRET]: PROXY_SECRET_HEX,
[CoreHeaders.OP]: ProxyOps.GET,
[CoreHeaders.OP_TARGET]: this.#stringifiedTarget,
[CoreHeaders.OP_KEY]: key,
Expand Down Expand Up @@ -416,6 +423,7 @@ class ProxyStubHandler<T extends object> implements ProxyHandler<T> {
const syncRes = this.bridge.sync.fetch(this.bridge.url, {
method: "POST",
headers: {
[CoreHeaders.OP_SECRET]: PROXY_SECRET_HEX,
[CoreHeaders.OP]: ProxyOps.GET_OWN_DESCRIPTOR,
[CoreHeaders.OP_KEY]: key,
[CoreHeaders.OP_TARGET]: this.#stringifiedTarget,
Expand All @@ -440,6 +448,7 @@ class ProxyStubHandler<T extends object> implements ProxyHandler<T> {
const syncRes = this.bridge.sync.fetch(this.bridge.url, {
method: "POST",
headers: {
[CoreHeaders.OP_SECRET]: PROXY_SECRET_HEX,
[CoreHeaders.OP]: ProxyOps.GET_OWN_KEYS,
[CoreHeaders.OP_TARGET]: this.#stringifiedTarget,
},
Expand Down Expand Up @@ -526,6 +535,7 @@ class ProxyStubHandler<T extends object> implements ProxyHandler<T> {
const syncRes = this.bridge.sync.fetch(this.bridge.url, {
method: "POST",
headers: {
[CoreHeaders.OP_SECRET]: PROXY_SECRET_HEX,
[CoreHeaders.OP]: ProxyOps.CALL,
[CoreHeaders.OP_TARGET]: this.#stringifiedTarget,
[CoreHeaders.OP_KEY]: key,
Expand All @@ -548,6 +558,7 @@ class ProxyStubHandler<T extends object> implements ProxyHandler<T> {
resPromise = this.bridge.dispatchFetch(this.bridge.url, {
method: "POST",
headers: {
[CoreHeaders.OP_SECRET]: PROXY_SECRET_HEX,
[CoreHeaders.OP]: ProxyOps.CALL,
[CoreHeaders.OP_TARGET]: this.#stringifiedTarget,
[CoreHeaders.OP_KEY]: key,
Expand All @@ -563,6 +574,7 @@ class ProxyStubHandler<T extends object> implements ProxyHandler<T> {
resPromise = this.bridge.dispatchFetch(this.bridge.url, {
method: "POST",
headers: {
[CoreHeaders.OP_SECRET]: PROXY_SECRET_HEX,
[CoreHeaders.OP]: ProxyOps.CALL,
[CoreHeaders.OP_TARGET]: this.#stringifiedTarget,
[CoreHeaders.OP_KEY]: key,
Expand All @@ -581,6 +593,7 @@ class ProxyStubHandler<T extends object> implements ProxyHandler<T> {
const request = new Request(...args);
// If adding new headers here, remember to `delete()` them in `ProxyServer`
// before calling `fetch()`.
request.headers.set(CoreHeaders.OP_SECRET, PROXY_SECRET_HEX);
request.headers.set(CoreHeaders.OP, ProxyOps.CALL);
request.headers.set(CoreHeaders.OP_TARGET, this.#stringifiedTarget);
request.headers.set(CoreHeaders.OP_KEY, "fetch");
Expand Down
2 changes: 2 additions & 0 deletions packages/miniflare/src/plugins/d1/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
PersistenceSchema,
Plugin,
SERVICE_LOOPBACK,
getControlEndpointBindings,
getPersistPath,
kProxyNodeBinding,
migrateDatabase,
Expand Down Expand Up @@ -117,6 +118,7 @@ export const D1_PLUGIN: Plugin<
name: SharedBindings.MAYBE_SERVICE_LOOPBACK,
service: { name: SERVICE_LOOPBACK },
},
...getControlEndpointBindings(),
],
},
};
Expand Down
2 changes: 2 additions & 0 deletions packages/miniflare/src/plugins/kv/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
PersistenceSchema,
Plugin,
SERVICE_LOOPBACK,
getControlEndpointBindings,
getPersistPath,
kProxyNodeBinding,
migrateDatabase,
Expand Down Expand Up @@ -123,6 +124,7 @@ export const KV_PLUGIN: Plugin<
name: SharedBindings.MAYBE_SERVICE_LOOPBACK,
service: { name: SERVICE_LOOPBACK },
},
...getControlEndpointBindings(),
],
},
};
Expand Down
2 changes: 2 additions & 0 deletions packages/miniflare/src/plugins/queues/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { getUserServiceName } from "../core";
import {
Plugin,
SERVICE_LOOPBACK,
getControlEndpointBindings,
kProxyNodeBinding,
namespaceEntries,
namespaceKeys,
Expand Down Expand Up @@ -87,6 +88,7 @@ export const QUEUES_PLUGIN: Plugin<typeof QueuesOptionsSchema> = {
name: SharedBindings.MAYBE_SERVICE_LOOPBACK,
service: { name: SERVICE_LOOPBACK },
},
...getControlEndpointBindings(),
{
name: SharedBindings.DURABLE_OBJECT_NAMESPACE_OBJECT,
durableObjectNamespace: {
Expand Down
2 changes: 2 additions & 0 deletions packages/miniflare/src/plugins/r2/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
PersistenceSchema,
Plugin,
SERVICE_LOOPBACK,
getControlEndpointBindings,
getPersistPath,
kProxyNodeBinding,
migrateDatabase,
Expand Down Expand Up @@ -97,6 +98,7 @@ export const R2_PLUGIN: Plugin<
name: SharedBindings.MAYBE_SERVICE_LOOPBACK,
service: { name: SERVICE_LOOPBACK },
},
...getControlEndpointBindings(),
],
},
};
Expand Down
14 changes: 14 additions & 0 deletions packages/miniflare/src/plugins/shared/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,20 @@ export const WORKER_BINDING_SERVICE_LOOPBACK: Worker_Binding = {
service: { name: SERVICE_LOOPBACK },
};

const WORKER_BINDING_ENABLE_CONTROL_ENDPOINT: Worker_Binding = {
name: SharedBindings.MAYBE_JSON_ENABLE_CONTROL_ENDPOINTS,
json: "true",
};
let enableControlEndpoints = false;
export function getControlEndpointBindings(): Worker_Binding[] {
if (enableControlEndpoints) return [WORKER_BINDING_ENABLE_CONTROL_ENDPOINT];
else return [];
}
/** @internal */
export function _enableControlEndpoints() {
enableControlEndpoints = true;
}

export function objectEntryWorker(
durableObjectNamespace: Worker_Binding_DurableObjectNamespaceDesignator,
namespace: string
Expand Down
2 changes: 2 additions & 0 deletions packages/miniflare/src/workers/core/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const CoreHeaders = {
ROUTE_OVERRIDE: "MF-Route-Override",

// API Proxy
OP_SECRET: "MF-Op-Secret",
OP: "MF-Op",
OP_TARGET: "MF-Op-Target",
OP_KEY: "MF-Op-Key",
Expand All @@ -25,6 +26,7 @@ export const CoreBindings = {
JSON_LOG_LEVEL: "MINIFLARE_LOG_LEVEL",
DATA_LIVE_RELOAD_SCRIPT: "MINIFLARE_LIVE_RELOAD_SCRIPT",
DURABLE_OBJECT_NAMESPACE_PROXY: "MINIFLARE_PROXY",
DATA_PROXY_SECRET: "MINIFLARE_PROXY_SECRET",
} as const;

export const ProxyOps = {
Expand Down
21 changes: 20 additions & 1 deletion packages/miniflare/src/workers/core/proxy.worker.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import assert from "node:assert";
import { Buffer } from "node:buffer";
import { parse } from "devalue";
import { readPrefix, reduceError } from "miniflare:shared";
import {
CoreBindings,
CoreHeaders,
ProxyAddresses,
ProxyOps,
Expand Down Expand Up @@ -59,6 +61,10 @@ function getType(value: unknown) {
return Object.prototype.toString.call(value).slice(8, -1); // `[object <type>]`
}

type Env = Record<string, unknown> & {
[CoreBindings.DATA_PROXY_SECRET]: ArrayBuffer;
};

// TODO(someday): extract `ProxyServer` into component that could be used by
// other (user) Durable Objects
export class ProxyServer implements DurableObject {
Expand Down Expand Up @@ -105,7 +111,10 @@ export class ProxyServer implements DurableObject {
};
nativeReviver: ReducersRevivers = { Native: this.revivers.Native };

constructor(_state: DurableObjectState, env: Record<string, unknown>) {
constructor(
_state: DurableObjectState,
readonly env: Env
) {
this.heap.set(ProxyAddresses.GLOBAL, globalThis);
this.heap.set(ProxyAddresses.ENV, env);
}
Expand All @@ -123,6 +132,15 @@ export class ProxyServer implements DurableObject {
}

async #fetch(request: Request) {
// Validate secret header to prevent unauthorised access to proxy
const secretHex = request.headers.get(CoreHeaders.OP_SECRET);
if (secretHex == null) return new Response(null, { status: 401 });
const expectedSecret = this.env[CoreBindings.DATA_PROXY_SECRET];
const secretBuffer = Buffer.from(secretHex, "hex");
if (!crypto.subtle.timingSafeEqual(secretBuffer, expectedSecret)) {
return new Response(null, { status: 401 });
}

const opHeader = request.headers.get(CoreHeaders.OP);
const targetHeader = request.headers.get(CoreHeaders.OP_TARGET);
const keyHeader = request.headers.get(CoreHeaders.OP_KEY);
Expand Down Expand Up @@ -188,6 +206,7 @@ export class ProxyServer implements DurableObject {
const url = new URL(originalUrl ?? request.url);
// Create a new request to allow header mutation and use original URL
request = new Request(url, request);
request.headers.delete(CoreHeaders.OP_SECRET);
request.headers.delete(CoreHeaders.OP);
request.headers.delete(CoreHeaders.OP_TARGET);
request.headers.delete(CoreHeaders.OP_KEY);
Expand Down
1 change: 1 addition & 0 deletions packages/miniflare/src/workers/shared/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const SharedBindings = {
DURABLE_OBJECT_NAMESPACE_OBJECT: "MINIFLARE_OBJECT",
MAYBE_SERVICE_BLOBS: "MINIFLARE_BLOBS",
MAYBE_SERVICE_LOOPBACK: "MINIFLARE_LOOPBACK",
MAYBE_JSON_ENABLE_CONTROL_ENDPOINTS: "MINIFLARE_ENABLE_CONTROL_ENDPOINTS",
} as const;

export enum LogLevel {
Expand Down
10 changes: 8 additions & 2 deletions packages/miniflare/src/workers/shared/object.worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ export interface MiniflareDurableObjectEnv {
// NOTE: this binding is optional so simulators can run standalone, without
// a Node.js loopback server. In this case, logging is a no-op.
[SharedBindings.MAYBE_SERVICE_LOOPBACK]?: Fetcher;
// If set to `true`, Miniflare enables additional endpoints in Durable Objects
// for testing. Note these endpoints allow anyone with access to the Miniflare
// dev server to run arbitrary SQL queries and read arbitrary blobs.
[SharedBindings.MAYBE_JSON_ENABLE_CONTROL_ENDPOINTS]?: boolean;
}

export interface MiniflareDurableObjectCfControlOp {
Expand Down Expand Up @@ -119,8 +123,10 @@ export abstract class MiniflareDurableObject<
async fetch(req: Request<unknown, MiniflareDurableObjectCf>) {
// Allow control of object internals by specifying operations in the `cf`
// object. Used by tests to update fake time, and access internal storage.
const controlOp = req?.cf?.miniflare?.controlOp;
if (controlOp !== undefined) return this.#handleControlOp(controlOp);
if (this.env[SharedBindings.MAYBE_JSON_ENABLE_CONTROL_ENDPOINTS] === true) {
const controlOp = req?.cf?.miniflare?.controlOp;
if (controlOp !== undefined) return this.#handleControlOp(controlOp);
}

// Each regular request to a `MiniflareDurableObject` includes the object
// ID's name, so we can create the `BlobStore`. Note, we could just use the
Expand Down
11 changes: 11 additions & 0 deletions packages/miniflare/test/plugins/core/proxy/client.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
ReplaceWorkersTypes,
Response,
WebSocketPair,
fetch,
} from "miniflare";

// This file tests API proxy edge cases. Cache, D1, Durable Object and R2 tests
Expand Down Expand Up @@ -265,3 +266,13 @@ test("ProxyClient: can `JSON.stringify()` proxies", async (t) => {
version: object.version,
});
});

test("ProxyServer: prevents unauthorised access", async (t) => {
const mf = new Miniflare({ script: nullScript });
t.teardown(() => mf.dispose());

const url = await mf.ready;
const res = await fetch(url, { headers: { "MF-Op": "GET" } });
t.is(res.status, 401);
await res.arrayBuffer(); // (drain)
});
7 changes: 6 additions & 1 deletion packages/miniflare/test/setup.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import Module from "node:module";
import { _initialiseInstanceRegistry } from "../dist/src/index.js";
import {
_initialiseInstanceRegistry,
_enableControlEndpoints,
} from "../dist/src/index.js";
import { fileURLToPath } from "node:url";
import path from "node:path";

Expand All @@ -20,6 +23,8 @@ const registry = _initialiseInstanceRegistry();
const bigSeparator = "=".repeat(80);
const separator = "-".repeat(80);

_enableControlEndpoints();

// `process.on("exit")` is more like `worker_thread.on(`exit`)` here. It will
// be called once AVA's finished running tests and `after` hooks. Note we can't
// use an `after` hook here, as that would run before `miniflareTest`'s
Expand Down
2 changes: 1 addition & 1 deletion packages/wrangler/src/__tests__/configuration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ describe("normalizeAndValidateConfig()", () => {
constellation: [],
hyperdrive: [],
dev: {
ip: "*",
ip: "localhost",
local_protocol: "http",
port: undefined, // the default of 8787 is set at runtime
upstream_protocol: "https",
Expand Down
Loading

0 comments on commit 311ffbd

Please sign in to comment.