Skip to content

Commit 7a57c14

Browse files
V3 Backport (fix(wrangler): strip CF-Connecting-IP header from all outbound requests #7914) (#8681)
* Strip CF-Connecting-IP * Create cyan-years-relate.md * Address comments * remove test * re-add test * fix tests * fix miniflare.ts * fix compat date --------- Co-authored-by: Samuel Macleod <[email protected]>
1 parent 4e943b1 commit 7a57c14

File tree

6 files changed

+100
-13
lines changed

6 files changed

+100
-13
lines changed

.changeset/cyan-years-relate.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"miniflare": patch
3+
"wrangler": patch
4+
---
5+
6+
fix(miniflare): strip CF-Connecting-IP header from all outbound requests

packages/miniflare/src/plugins/core/index.ts

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { TextEncoder } from "util";
88
import { bold } from "kleur/colors";
99
import { MockAgent } from "undici";
1010
import SCRIPT_ENTRY from "worker:core/entry";
11+
import STRIP_CF_CONNECTING_IP from "worker:core/strip-cf-connecting-ip";
1112
import { z } from "zod";
1213
import { fetch } from "../../http";
1314
import {
@@ -160,6 +161,9 @@ const CoreOptionsSchemaInput = z.intersection(
160161
*/
161162
hasAssetsAndIsVitest: z.boolean().optional(),
162163
unsafeEnableAssetsRpc: z.boolean().optional(),
164+
165+
// Strip the CF-Connecting-IP header from outbound fetches
166+
stripCfConnectingIp: z.boolean().default(true),
163167
})
164168
);
165169
export const CoreOptionsSchema = CoreOptionsSchemaInput.transform((value) => {
@@ -387,6 +391,27 @@ export function maybeWrappedModuleToWorkerName(
387391
}
388392
}
389393

394+
function getStripCfConnectingIpName(workerIndex: number) {
395+
return `strip-cf-connecting-ip:${workerIndex}`;
396+
}
397+
398+
function getGlobalOutbound(
399+
workerIndex: number,
400+
options: z.infer<typeof CORE_PLUGIN.options>
401+
) {
402+
return options.outboundService === undefined
403+
? undefined
404+
: getCustomServiceDesignator(
405+
/* referrer */ options.name,
406+
workerIndex,
407+
CustomServiceKind.KNOWN,
408+
CUSTOM_SERVICE_KNOWN_OUTBOUND,
409+
options.outboundService,
410+
options.hasAssetsAndIsVitest,
411+
options.unsafeEnableAssetsRpc
412+
);
413+
}
414+
390415
export const CORE_PLUGIN: Plugin<
391416
typeof CoreOptionsSchema,
392417
typeof CoreSharedOptionsSchema
@@ -686,18 +711,9 @@ export const CORE_PLUGIN: Plugin<
686711
: options.unsafeEphemeralDurableObjects
687712
? { inMemory: kVoid }
688713
: { localDisk: DURABLE_OBJECTS_STORAGE_SERVICE_NAME },
689-
globalOutbound:
690-
options.outboundService === undefined
691-
? undefined
692-
: getCustomServiceDesignator(
693-
/* referrer */ options.name,
694-
workerIndex,
695-
CustomServiceKind.KNOWN,
696-
CUSTOM_SERVICE_KNOWN_OUTBOUND,
697-
options.outboundService,
698-
options.hasAssetsAndIsVitest,
699-
options.unsafeEnableAssetsRpc
700-
),
714+
globalOutbound: options.stripCfConnectingIp
715+
? { name: getStripCfConnectingIpName(workerIndex) }
716+
: getGlobalOutbound(workerIndex, options),
701717
cacheApiOutbound: { name: getCacheServiceName(workerIndex) },
702718
moduleFallback:
703719
options.unsafeUseModuleFallbackService &&
@@ -730,6 +746,22 @@ export const CORE_PLUGIN: Plugin<
730746
if (maybeService !== undefined) services.push(maybeService);
731747
}
732748

749+
if (options.stripCfConnectingIp) {
750+
services.push({
751+
name: getStripCfConnectingIpName(workerIndex),
752+
worker: {
753+
modules: [
754+
{
755+
name: "index.js",
756+
esModule: STRIP_CF_CONNECTING_IP(),
757+
},
758+
],
759+
compatibilityDate: "2025-01-01",
760+
globalOutbound: getGlobalOutbound(workerIndex, options),
761+
},
762+
});
763+
}
764+
733765
return { services, extensions };
734766
},
735767
};
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export default {
2+
fetch(request) {
3+
const headers = new Headers(request.headers);
4+
headers.delete("CF-Connecting-IP");
5+
return fetch(request, { headers });
6+
},
7+
} satisfies ExportedHandler;

packages/miniflare/test/index.spec.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2703,6 +2703,46 @@ test("Miniflare: CF-Connecting-IP is preserved when present", async (t) => {
27032703
t.deepEqual(await ip.text(), "128.0.0.1");
27042704
});
27052705

2706+
// regression test for https://github.com/cloudflare/workers-sdk/issues/7924
2707+
test("Miniflare: strips CF-Connecting-IP", async (t) => {
2708+
const server = new Miniflare({
2709+
script:
2710+
"export default { fetch(request) { return new Response(request.headers.get(`CF-Connecting-IP`)) } }",
2711+
modules: true,
2712+
});
2713+
const url = await server.ready;
2714+
2715+
const client = new Miniflare({
2716+
script: `export default { fetch(request) { return fetch('${url.href}', {headers: {"CF-Connecting-IP":"fake-value"}}) } }`,
2717+
modules: true,
2718+
});
2719+
t.teardown(() => client.dispose());
2720+
t.teardown(() => server.dispose());
2721+
2722+
const landingPage = await client.dispatchFetch("http://example.com/");
2723+
// The CF-Connecting-IP header value of "fake-value" should be stripped by Miniflare, and should be replaced with a generic 127.0.0.1
2724+
t.notDeepEqual(await landingPage.text(), "fake-value");
2725+
});
2726+
test("Miniflare: does not strip CF-Connecting-IP when configured", async (t) => {
2727+
const server = new Miniflare({
2728+
script:
2729+
"export default { fetch(request) { return new Response(request.headers.get(`CF-Connecting-IP`)) } }",
2730+
modules: true,
2731+
});
2732+
const url = await server.ready;
2733+
2734+
const client = new Miniflare({
2735+
script: `export default { fetch(request) { return fetch('${url.href}', {headers: {"CF-Connecting-IP":"fake-value"}}) } }`,
2736+
modules: true,
2737+
stripCfConnectingIp: false,
2738+
});
2739+
t.teardown(() => client.dispose());
2740+
t.teardown(() => server.dispose());
2741+
2742+
const landingPage = await client.dispatchFetch("http://example.com/");
2743+
t.deepEqual(await landingPage.text(), "fake-value");
2744+
});
2745+
27062746
test("Miniflare: can use module fallback service", async (t) => {
27072747
const modulesRoot = "/";
27082748
const modules: Record<string, Omit<Worker_Module, "name">> = {

packages/miniflare/turbo.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
"extends": ["//"],
44
"tasks": {
55
"build": {
6-
"inputs": ["src/**", "scripts/**", "types/**", "*.mjs", "*.js", "*.json"],
76
"outputs": ["dist/**", "bootstrap.js", "worker-metafiles/**"],
87
"env": ["CI_OS"]
98
},

packages/wrangler/src/api/startDevWorker/ProxyController.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,9 @@ export class ProxyController extends Controller<ProxyControllerEventMap> {
9494
unsafePreventEviction: true,
9595
},
9696
},
97+
// Miniflare will strip CF-Connecting-IP from outgoing fetches from a Worker (to fix https://github.com/cloudflare/workers-sdk/issues/7924)
98+
// However, the proxy worker only makes outgoing requests to the user Worker Miniflare instance, which _should_ receive CF-Connecting-IP
99+
stripCfConnectingIp: false,
97100
serviceBindings: {
98101
PROXY_CONTROLLER: async (req): Promise<Response> => {
99102
const message =

0 commit comments

Comments
 (0)