Skip to content

Commit 37af035

Browse files
andyjessoppenalosa
andauthored
Strip CF-Connecting-IP header from all outbound requests (#7914)
* Strip CF-Connecting-IP * Create cyan-years-relate.md * Address comments * remove test * re-add test * fix tests * fix miniflare.ts * fix compat date * test fixups --------- Co-authored-by: Samuel Macleod <[email protected]>
1 parent 2cc8197 commit 37af035

File tree

6 files changed

+104
-12
lines changed

6 files changed

+104
-12
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 & 11 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
hasAssetsAndIsVitest: z.boolean().optional(),
161162

162163
tails: z.array(ServiceDesignatorSchema).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) => {
@@ -390,6 +394,26 @@ export function maybeWrappedModuleToWorkerName(
390394
}
391395
}
392396

397+
function getStripCfConnectingIpName(workerIndex: number) {
398+
return `strip-cf-connecting-ip:${workerIndex}`;
399+
}
400+
401+
function getGlobalOutbound(
402+
workerIndex: number,
403+
options: z.infer<typeof CORE_PLUGIN.options>
404+
) {
405+
return options.outboundService === undefined
406+
? undefined
407+
: getCustomServiceDesignator(
408+
/* referrer */ options.name,
409+
workerIndex,
410+
CustomServiceKind.KNOWN,
411+
CUSTOM_SERVICE_KNOWN_OUTBOUND,
412+
options.outboundService,
413+
options.hasAssetsAndIsVitest
414+
);
415+
}
416+
393417
export const CORE_PLUGIN: Plugin<
394418
typeof CoreOptionsSchema,
395419
typeof CoreSharedOptionsSchema
@@ -689,17 +713,9 @@ export const CORE_PLUGIN: Plugin<
689713
: options.unsafeEphemeralDurableObjects
690714
? { inMemory: kVoid }
691715
: { localDisk: DURABLE_OBJECTS_STORAGE_SERVICE_NAME },
692-
globalOutbound:
693-
options.outboundService === undefined
694-
? undefined
695-
: getCustomServiceDesignator(
696-
/* referrer */ options.name,
697-
workerIndex,
698-
CustomServiceKind.KNOWN,
699-
CUSTOM_SERVICE_KNOWN_OUTBOUND,
700-
options.outboundService,
701-
options.hasAssetsAndIsVitest
702-
),
716+
globalOutbound: options.stripCfConnectingIp
717+
? { name: getStripCfConnectingIpName(workerIndex) }
718+
: getGlobalOutbound(workerIndex, options),
703719
cacheApiOutbound: { name: getCacheServiceName(workerIndex) },
704720
moduleFallback:
705721
options.unsafeUseModuleFallbackService &&
@@ -747,6 +763,7 @@ export const CORE_PLUGIN: Plugin<
747763
if (maybeService !== undefined) services.push(maybeService);
748764
}
749765
}
766+
750767
if (options.outboundService !== undefined) {
751768
const maybeService = maybeGetCustomServiceService(
752769
workerIndex,
@@ -757,6 +774,22 @@ export const CORE_PLUGIN: Plugin<
757774
if (maybeService !== undefined) services.push(maybeService);
758775
}
759776

777+
if (options.stripCfConnectingIp) {
778+
services.push({
779+
name: getStripCfConnectingIpName(workerIndex),
780+
worker: {
781+
modules: [
782+
{
783+
name: "index.js",
784+
esModule: STRIP_CF_CONNECTING_IP(),
785+
},
786+
],
787+
compatibilityDate: "2025-01-01",
788+
globalOutbound: getGlobalOutbound(workerIndex, options),
789+
},
790+
});
791+
}
792+
760793
return { services, extensions };
761794
},
762795
};
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: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2995,6 +2995,50 @@ test("Miniflare: CF-Connecting-IP is preserved when present", async (t) => {
29952995
t.deepEqual(await ip.text(), "128.0.0.1");
29962996
});
29972997

2998+
// regression test for https://github.com/cloudflare/workers-sdk/issues/7924
2999+
// The "server" service just returns the value of the CF-Connecting-IP header which would normally be added by Miniflare. If you send a request to with no such header, Miniflare will add one.
3000+
// The "client" service makes an outbound request with a fake CF-Connecting-IP header to the "server" service. If the outbound stripping happens then this header will not make it to the "server" service
3001+
// so its response will contain the header added by Miniflare. If the stripping is turned off then the response from the "server" service will contain the fake header.
3002+
test("Miniflare: strips CF-Connecting-IP", async (t) => {
3003+
const server = new Miniflare({
3004+
script:
3005+
"export default { fetch(request) { return new Response(request.headers.get(`CF-Connecting-IP`)) } }",
3006+
modules: true,
3007+
});
3008+
const serverUrl = await server.ready;
3009+
3010+
const client = new Miniflare({
3011+
script: `export default { fetch(request) { return fetch('${serverUrl.href}', {headers: {"CF-Connecting-IP":"fake-value"}}) } }`,
3012+
modules: true,
3013+
});
3014+
t.teardown(() => client.dispose());
3015+
t.teardown(() => server.dispose());
3016+
3017+
const landingPage = await client.dispatchFetch("http://example.com/");
3018+
// 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
3019+
t.notDeepEqual(await landingPage.text(), "fake-value");
3020+
});
3021+
3022+
test("Miniflare: does not strip CF-Connecting-IP when configured", async (t) => {
3023+
const server = new Miniflare({
3024+
script:
3025+
"export default { fetch(request) { return new Response(request.headers.get(`CF-Connecting-IP`)) } }",
3026+
modules: true,
3027+
});
3028+
const serverUrl = await server.ready;
3029+
3030+
const client = new Miniflare({
3031+
script: `export default { fetch(request) { return fetch('${serverUrl.href}', {headers: {"CF-Connecting-IP":"fake-value"}}) } }`,
3032+
modules: true,
3033+
stripCfConnectingIp: false,
3034+
});
3035+
t.teardown(() => client.dispose());
3036+
t.teardown(() => server.dispose());
3037+
3038+
const landingPage = await client.dispatchFetch("http://example.com/");
3039+
t.deepEqual(await landingPage.text(), "fake-value");
3040+
});
3041+
29983042
test("Miniflare: can use module fallback service", async (t) => {
29993043
const modulesRoot = "/";
30003044
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": ["$TURBO_DEFAULT$", "!test/**", "!ava.config.mjs"],
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)