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.
This commit is contained in:
Antigravity Assistant
2026-04-30 14:03:15 -03:00
parent 9d9d1774c7
commit b5ae2af150
4 changed files with 69 additions and 22 deletions

View File

@@ -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() {
</span>
<input
value={port}
onChange={(event) => 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"
/>

View File

@@ -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") {

View File

@@ -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.",

View File

@@ -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", {