mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
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:
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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", {
|
||||
|
||||
Reference in New Issue
Block a user