From f5655394deeaf8750f574640a0a359c1de4eeb01 Mon Sep 17 00:00:00 2001 From: luvs01 Date: Sun, 16 Aug 2026 12:51:44 +0900 Subject: [PATCH] fix(minimax): pin bridge traffic to loopback --- src/cli/minimax.ts | 10 ++++-- tests/fixtures/minimax-bridge-direct.ts | 41 +++++++++++++++++++++++++ tests/minimax-clients.test.ts | 40 ++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 tests/fixtures/minimax-bridge-direct.ts diff --git a/src/cli/minimax.ts b/src/cli/minimax.ts index 92d2bef311..69d3cfe8df 100644 --- a/src/cli/minimax.ts +++ b/src/cli/minimax.ts @@ -190,12 +190,18 @@ export function startMmxTextBridge( ? null : clearableDeadline(options.headerTimeoutMs, req.signal); try { - return await fetch(new Request(target, { + const upstreamRequest = new Request(target, { method: "POST", headers, body: req.body, signal: headerDeadline?.signal ?? req.signal, - })); + }); + return await fetch(upstreamRequest, { + // Override HTTP(S)_PROXY with the loopback listener itself. Bun sends + // the HTTP proxy-form request directly to this exact origin, so the + // hop cannot leave the machine even when the parent has proxy vars. + proxy: { url: upstreamOrigin }, + }); } catch { return Response.json({ type: "error", diff --git a/tests/fixtures/minimax-bridge-direct.ts b/tests/fixtures/minimax-bridge-direct.ts new file mode 100644 index 0000000000..dcce715eb6 --- /dev/null +++ b/tests/fixtures/minimax-bridge-direct.ts @@ -0,0 +1,41 @@ +import { startMmxTextBridge } from "../../src/cli/minimax"; + +let proxyRequests = 0; +let upstreamBody = ""; +const upstream = Bun.serve({ + hostname: "127.0.0.1", + port: 0, + async fetch(request) { + upstreamBody = await request.text(); + return Response.json({ source: "upstream" }); + }, +}); +const attackerProxy = Bun.serve({ + hostname: "127.0.0.1", + port: Number(process.env.TEST_PROXY_PORT), + fetch() { + proxyRequests += 1; + return Response.json({ source: "proxy" }); + }, +}); +const bridge = startMmxTextBridge({ hostname: "127.0.0.1", port: upstream.port }); + +try { + const response = await fetch(`${bridge.baseUrl}/anthropic/v1/messages`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ prompt: "private bridge payload" }), + proxy: { url: bridge.baseUrl }, + }); + const responseText = await response.text(); + console.log(JSON.stringify({ + responseStatus: response.status, + responseText, + proxyRequests, + upstreamBody, + })); +} finally { + await bridge.stop(); + await upstream.stop(true); + await attackerProxy.stop(true); +} diff --git a/tests/minimax-clients.test.ts b/tests/minimax-clients.test.ts index 4203731747..e72632395a 100644 --- a/tests/minimax-clients.test.ts +++ b/tests/minimax-clients.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from "bun:test"; import { EventEmitter } from "node:events"; +import { createServer } from "node:net"; import { join } from "node:path"; import { ClientPathError, @@ -90,6 +91,45 @@ describe("MiniMax Code client config", () => { }); describe("MiniMax CLI wrapper", () => { + test("keeps the bridge's upstream hop direct when the parent has a proxy", async () => { + const reservation = createServer(); + const proxyPort = await new Promise((resolve, reject) => { + reservation.once("error", reject); + reservation.listen(0, "127.0.0.1", () => { + const address = reservation.address(); + if (!address || typeof address === "string") reject(new Error("proxy port reservation failed")); + else resolve(address.port); + }); + }); + await new Promise((resolve, reject) => reservation.close(error => error ? reject(error) : resolve())); + + const child = Bun.spawn([process.execPath, join(import.meta.dir, "fixtures/minimax-bridge-direct.ts")], { + env: { + ...process.env, + HTTP_PROXY: `http://127.0.0.1:${proxyPort}`, + HTTPS_PROXY: `http://127.0.0.1:${proxyPort}`, + ALL_PROXY: `http://127.0.0.1:${proxyPort}`, + NO_PROXY: "", + no_proxy: "", + TEST_PROXY_PORT: String(proxyPort), + }, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([ + new Response(child.stdout).text(), + new Response(child.stderr).text(), + child.exited, + ]); + expect(exitCode, stderr).toBe(0); + expect(JSON.parse(stdout.trim())).toEqual({ + responseStatus: 200, + responseText: JSON.stringify({ source: "upstream" }), + proxyRequests: 0, + upstreamBody: JSON.stringify({ prompt: "private bridge payload" }), + }); + }); + test("passes through only standalone help and officially supported version invocations", () => { expect(isStandaloneInformationalInvocation(["--help"], "mmx")).toBeTrue(); expect(isStandaloneInformationalInvocation(["--version"], "mmx")).toBeTrue();