From b5ae2af150650448fb5bee624e7debaa36d88a90 Mon Sep 17 00:00:00 2001 From: Antigravity Assistant Date: Thu, 30 Apr 2026 14:03:15 -0300 Subject: [PATCH] fix(mitm): enforce transparent interception on port 443 only Reject non-443 MITM port updates in the settings API and normalize stored configuration back to the required transparent interception port. Lock the dashboard port field to 443, update the validation copy, and add integration coverage to prevent stale custom ports from being accepted or surfaced. --- .../settings/components/MitmProxyTab.tsx | 15 ++++--- src/app/api/settings/mitm/route.ts | 29 ++++++------ src/i18n/messages/en.json | 2 +- tests/integration/api-routes-critical.test.ts | 45 +++++++++++++++++++ 4 files changed, 69 insertions(+), 22 deletions(-) diff --git a/src/app/(dashboard)/dashboard/settings/components/MitmProxyTab.tsx b/src/app/(dashboard)/dashboard/settings/components/MitmProxyTab.tsx index 0ee4cd7c26..29f2d44c24 100644 --- a/src/app/(dashboard)/dashboard/settings/components/MitmProxyTab.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/MitmProxyTab.tsx @@ -4,6 +4,8 @@ import { useCallback, useEffect, useState } from "react"; import { useTranslations } from "next-intl"; import { Card } from "@/shared/components"; +const TRANSPARENT_MITM_PORT = 443; + type MitmTargetRoute = { id: string; name: string; @@ -80,7 +82,7 @@ export default function MitmProxyTab() { const data = await response.json().catch(() => ({})); if (!response.ok) throw new Error(data.error || t("loadFailed")); setStatus(data); - setPort(String(data.port || 443)); + setPort(String(TRANSPARENT_MITM_PORT)); setFeedback(null); } catch (error) { setFeedback({ @@ -108,7 +110,7 @@ export default function MitmProxyTab() { const data = await response.json().catch(() => ({})); if (!response.ok) throw new Error(data.error || t("saveFailed")); setStatus(data); - setPort(String(data.port || 443)); + setPort(String(TRANSPARENT_MITM_PORT)); setFeedback({ type: "success", message: successMessage }); } catch (error) { setFeedback({ @@ -122,18 +124,19 @@ export default function MitmProxyTab() { const savePort = () => { const parsedPort = Number.parseInt(port, 10); - if (!Number.isInteger(parsedPort) || parsedPort < 1 || parsedPort > 65535) { + if (parsedPort !== TRANSPARENT_MITM_PORT) { setFeedback({ type: "error", message: t("invalidPort") }); + setPort(String(TRANSPARENT_MITM_PORT)); return; } - void updateMitm({ port: parsedPort }, t("settingsSaved")); + void updateMitm({ port: TRANSPARENT_MITM_PORT }, t("settingsSaved")); }; const toggleMitm = () => { void updateMitm( { enabled: !status.running, - port: Number.parseInt(port, 10) || 443, + port: TRANSPARENT_MITM_PORT, apiKey: String(apiKey || "").trim() || undefined, sudoPassword: sudoPassword || undefined, }, @@ -240,7 +243,7 @@ export default function MitmProxyTab() { setPort(event.target.value)} + readOnly disabled={status.running} className="w-full rounded-lg border border-border bg-surface px-3 py-2 text-sm text-text-main focus:outline-none focus:ring-2 focus:ring-primary/40 disabled:opacity-60" /> diff --git a/src/app/api/settings/mitm/route.ts b/src/app/api/settings/mitm/route.ts index 4509291a9f..247742ff69 100644 --- a/src/app/api/settings/mitm/route.ts +++ b/src/app/api/settings/mitm/route.ts @@ -34,6 +34,8 @@ type MitmConfig = { }; const DEFAULT_PORT = 443; +const MITM_PORT_ERROR = + "Transparent MITM interception currently requires port 443 because DNS override does not redirect destination ports."; const updateMitmSchema = z.object({ enabled: z.boolean().optional(), @@ -92,17 +94,10 @@ function defaultTargets(port = DEFAULT_PORT): MitmTargetRoute[] { function readConfig(): MitmConfig { try { - const raw = JSON.parse(fs.readFileSync(getConfigPath(), "utf8")); - const port = - typeof raw.port === "number" && - Number.isInteger(raw.port) && - raw.port > 0 && - raw.port <= 65535 - ? raw.port - : DEFAULT_PORT; + JSON.parse(fs.readFileSync(getConfigPath(), "utf8")); return { - port, - targets: defaultTargets(port), + port: DEFAULT_PORT, + targets: defaultTargets(DEFAULT_PORT), }; } catch { return { @@ -112,10 +107,10 @@ function readConfig(): MitmConfig { } } -function writeConfig(config: MitmConfig) { +function writeConfig() { const mitmDir = getMitmDir(); fs.mkdirSync(mitmDir, { recursive: true }); - fs.writeFileSync(getConfigPath(), JSON.stringify({ port: config.port }, null, 2)); + fs.writeFileSync(getConfigPath(), JSON.stringify({ port: DEFAULT_PORT }, null, 2)); } function readStats(): MitmStats { @@ -197,10 +192,14 @@ export async function PUT(request: Request) { } const config = readConfig(); - if (parsed.data.port) { - config.port = parsed.data.port; + if (parsed.data.port !== undefined && parsed.data.port !== DEFAULT_PORT) { + return NextResponse.json({ error: MITM_PORT_ERROR }, { status: 400 }); + } + + if (parsed.data.port !== undefined) { + config.port = DEFAULT_PORT; config.targets = defaultTargets(config.port); - writeConfig(config); + writeConfig(); } if (typeof parsed.data.enabled === "boolean") { diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 7c5c207a08..24cebefcfd 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -2445,7 +2445,7 @@ "stoppedSuccess": "MITM proxy stopped.", "saveFailed": "Failed to update MITM settings.", "loadFailed": "Failed to load MITM settings.", - "invalidPort": "Port must be between 1 and 65535.", + "invalidPort": "Transparent MITM interception currently requires port 443.", "certificate": "CA Certificate", "certificateReady": "Certificate is available for client trust installation.", "certificateMissing": "Certificate has not been generated yet.", diff --git a/tests/integration/api-routes-critical.test.ts b/tests/integration/api-routes-critical.test.ts index 4ab8291f7a..cebdfc3253 100644 --- a/tests/integration/api-routes-critical.test.ts +++ b/tests/integration/api-routes-critical.test.ts @@ -14,6 +14,7 @@ const apiKeysDb = await import("../../src/lib/db/apiKeys.ts"); const localDb = await import("../../src/lib/localDb.ts"); const proxiesRoute = await import("../../src/app/api/v1/management/proxies/route.ts"); const settingsProxyRoute = await import("../../src/app/api/settings/proxy/route.ts"); +const settingsMitmRoute = await import("../../src/app/api/settings/mitm/route.ts"); const v1ModelsRoute = await import("../../src/app/api/v1/models/route.ts"); const MACHINE_ID = "1234567890abcdef"; @@ -422,6 +423,50 @@ test("critical routes: settings proxy prefers registry assignment for global loo assert.equal(body.proxy.username, "global-user"); }); +test("critical routes: MITM settings reject non-443 transparent interception ports", async () => { + await enableManagementAuth(); + + const mitmDir = path.join(TEST_DATA_DIR, "mitm"); + fs.mkdirSync(mitmDir, { recursive: true }); + fs.writeFileSync(path.join(mitmDir, "settings.json"), JSON.stringify({ port: 9443 })); + + const staleConfig = await settingsMitmRoute.GET( + await makeManagementSessionRequest("http://localhost/api/settings/mitm") + ); + const staleConfigBody = (await staleConfig.json()) as any; + + const invalidPort = await settingsMitmRoute.PUT( + await makeManagementSessionRequest("http://localhost/api/settings/mitm", { + method: "PUT", + body: { port: 9443 }, + }) + ); + const invalidPortBody = (await invalidPort.json()) as any; + + const validPort = await settingsMitmRoute.PUT( + await makeManagementSessionRequest("http://localhost/api/settings/mitm", { + method: "PUT", + body: { port: 443 }, + }) + ); + const validPortBody = (await validPort.json()) as any; + const staleAntigravityTarget = staleConfigBody.targets.find( + (target: any) => target.id === "antigravity" + ); + const validAntigravityTarget = validPortBody.targets.find( + (target: any) => target.id === "antigravity" + ); + + assert.equal(staleConfig.status, 200); + assert.equal(staleConfigBody.port, 443); + assert.equal(staleAntigravityTarget?.localPort, 443); + assert.equal(invalidPort.status, 400); + assert.match(invalidPortBody.error, /requires port 443/i); + assert.equal(validPort.status, 200); + assert.equal(validPortBody.port, 443); + assert.equal(validAntigravityTarget?.localPort, 443); +}); + test("critical routes: settings proxy covers global fallback and socks5 gating", async () => { const setLegacyGlobalProxy = await settingsProxyRoute.PUT( makeRequest("http://localhost/api/settings/proxy", {