mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-17 12:22:34 +03:00
fix(api): return public-safe errors from the tunnel and MITM routes (#11872)
Closes a real Hard Rule #12 violation — 14 catch blocks across the tunnel/MITM routes echoed a raw err.message, which for Tailscale could leak a live tskey-* credential and always disclosed host layout / OS account name. Routes all 14 sites through a new toPublicSafeTunnelError() classifier, verified by a dedicated regression suite (14/14 passing) asserting no route echoes a raw error.message. Thanks for the security fix!
This commit is contained in:
@@ -5,6 +5,7 @@ import path from "path";
|
||||
import { z } from "zod";
|
||||
import { NextResponse } from "next/server";
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
import { toPublicSafeTunnelError } from "@/lib/api/publicSafeTunnelError";
|
||||
import { resolveApiKey } from "@/shared/services/apiKeyResolver";
|
||||
import { resolveMitmDataDir } from "@/mitm/dataDir";
|
||||
import { KIRO_MITM_PROFILE } from "@/mitm/targets/kiro";
|
||||
@@ -180,8 +181,10 @@ export async function GET(request: Request) {
|
||||
|
||||
return NextResponse.json(await buildMitmResponse());
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Failed to load MITM settings";
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
return NextResponse.json(
|
||||
toPublicSafeTunnelError(error, "Failed to load the MITM settings.", "settings/mitm GET"),
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,8 +239,10 @@ export async function PUT(request: Request) {
|
||||
|
||||
return NextResponse.json(await buildMitmResponse());
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Failed to update MITM settings";
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
return NextResponse.json(
|
||||
toPublicSafeTunnelError(error, "Failed to update the MITM settings.", "settings/mitm PUT"),
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,8 +279,13 @@ export async function POST(request: Request) {
|
||||
|
||||
return NextResponse.json(await buildMitmResponse());
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error ? error.message : "Failed to regenerate MITM certificate";
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
return NextResponse.json(
|
||||
toPublicSafeTunnelError(
|
||||
error,
|
||||
"Failed to regenerate the MITM certificate.",
|
||||
"settings/mitm POST"
|
||||
),
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { isAuthenticated } from "@/shared/utils/apiAuth";
|
||||
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
|
||||
import {
|
||||
formatValidationMessage,
|
||||
isValidationFailure,
|
||||
validateBody,
|
||||
} from "@/shared/validation/helpers";
|
||||
import {
|
||||
getCloudflaredTunnelStatus,
|
||||
startCloudflaredTunnel,
|
||||
stopCloudflaredTunnel,
|
||||
} from "@/lib/cloudflaredTunnel";
|
||||
import { toPublicSafeTunnelError } from "@/lib/api/publicSafeTunnelError";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -28,9 +33,11 @@ export async function GET(request: NextRequest) {
|
||||
return NextResponse.json(status);
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: error instanceof Error ? error.message : "Failed to load cloudflared tunnel status",
|
||||
},
|
||||
toPublicSafeTunnelError(
|
||||
error,
|
||||
"Failed to load the cloudflared tunnel status.",
|
||||
"tunnels/cloudflared GET"
|
||||
),
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
@@ -50,7 +57,10 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
const validation = validateBody(actionSchema, rawBody);
|
||||
if (isValidationFailure(validation)) {
|
||||
return validation.response;
|
||||
// validateBody() returns { success, error } — it has no `response` field, so
|
||||
// the previous `return validation.response` returned undefined and Next
|
||||
// answered with a framework 500 instead of this 400.
|
||||
return NextResponse.json({ error: formatValidationMessage(validation.error) }, { status: 400 });
|
||||
}
|
||||
|
||||
const parsed = validation.data;
|
||||
@@ -66,9 +76,11 @@ export async function POST(request: NextRequest) {
|
||||
});
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: error instanceof Error ? error.message : "Failed to update cloudflared tunnel",
|
||||
},
|
||||
toPublicSafeTunnelError(
|
||||
error,
|
||||
"Failed to update the cloudflared tunnel.",
|
||||
"tunnels/cloudflared POST"
|
||||
),
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { isAuthenticated } from "@/shared/utils/apiAuth";
|
||||
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
|
||||
import {
|
||||
formatValidationMessage,
|
||||
isValidationFailure,
|
||||
validateBody,
|
||||
} from "@/shared/validation/helpers";
|
||||
import { getNgrokTunnelStatus, startNgrokTunnel, stopNgrokTunnel } from "@/lib/ngrokTunnel";
|
||||
import { toPublicSafeTunnelError } from "@/lib/api/publicSafeTunnelError";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -25,9 +30,11 @@ export async function GET(request: NextRequest) {
|
||||
return NextResponse.json(status);
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: error instanceof Error ? error.message : "Failed to load ngrok tunnel status",
|
||||
},
|
||||
toPublicSafeTunnelError(
|
||||
error,
|
||||
"Failed to load the ngrok tunnel status.",
|
||||
"tunnels/ngrok GET"
|
||||
),
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
@@ -47,7 +54,10 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
const validation = validateBody(actionSchema, rawBody);
|
||||
if (isValidationFailure(validation)) {
|
||||
return validation.response;
|
||||
// validateBody() returns { success, error } — it has no `response` field, so
|
||||
// the previous `return validation.response` returned undefined and Next
|
||||
// answered with a framework 500 instead of this 400.
|
||||
return NextResponse.json({ error: formatValidationMessage(validation.error) }, { status: 400 });
|
||||
}
|
||||
|
||||
const parsed = validation.data;
|
||||
@@ -65,9 +75,7 @@ export async function POST(request: NextRequest) {
|
||||
});
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: error instanceof Error ? error.message : "Failed to update ngrok tunnel",
|
||||
},
|
||||
toPublicSafeTunnelError(error, "Failed to update the ngrok tunnel.", "tunnels/ngrok POST"),
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getTailscaleCheckStatus } from "@/lib/tailscaleTunnel";
|
||||
import { toPublicSafeTunnelError } from "@/lib/api/publicSafeTunnelError";
|
||||
import { requireTailscaleAuth } from "../routeUtils";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
@@ -13,9 +14,11 @@ export async function GET(request: Request) {
|
||||
return NextResponse.json(status);
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: error instanceof Error ? error.message : "Failed to check Tailscale state",
|
||||
},
|
||||
toPublicSafeTunnelError(
|
||||
error,
|
||||
"Failed to check the Tailscale state.",
|
||||
"tunnels/tailscale/check GET"
|
||||
),
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { disableTailscaleTunnel } from "@/lib/tailscaleTunnel";
|
||||
import { toPublicSafeTunnelError } from "@/lib/api/publicSafeTunnelError";
|
||||
import { parseOptionalJsonBody, requireTailscaleAuth, tailscaleSudoSchema } from "../routeUtils";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
@@ -16,9 +17,11 @@ export async function POST(request: Request) {
|
||||
return NextResponse.json(result);
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: error instanceof Error ? error.message : "Failed to disable Tailscale Funnel",
|
||||
},
|
||||
toPublicSafeTunnelError(
|
||||
error,
|
||||
"Failed to disable the Tailscale Funnel.",
|
||||
"tunnels/tailscale/disable POST"
|
||||
),
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { enableTailscaleTunnel } from "@/lib/tailscaleTunnel";
|
||||
import { toPublicSafeTunnelError } from "@/lib/api/publicSafeTunnelError";
|
||||
import { parseOptionalJsonBody, requireTailscaleAuth, tailscaleEnableSchema } from "../routeUtils";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
@@ -16,9 +17,11 @@ export async function POST(request: Request) {
|
||||
return NextResponse.json(result);
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: error instanceof Error ? error.message : "Failed to enable Tailscale Funnel",
|
||||
},
|
||||
toPublicSafeTunnelError(
|
||||
error,
|
||||
"Failed to enable the Tailscale Funnel.",
|
||||
"tunnels/tailscale/enable POST"
|
||||
),
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getTailscaleTunnelStatus, installTailscale } from "@/lib/tailscaleTunnel";
|
||||
import { toPublicSafeTunnelError } from "@/lib/api/publicSafeTunnelError";
|
||||
import { parseOptionalJsonBody, requireTailscaleAuth, tailscaleSudoSchema } from "../routeUtils";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
@@ -31,9 +32,14 @@ export async function POST(request: Request) {
|
||||
status: await getTailscaleTunnelStatus(),
|
||||
});
|
||||
} catch (error) {
|
||||
pushEvent("error", {
|
||||
error: error instanceof Error ? error.message : "Failed to install Tailscale",
|
||||
});
|
||||
pushEvent(
|
||||
"error",
|
||||
toPublicSafeTunnelError(
|
||||
error,
|
||||
"Failed to install Tailscale.",
|
||||
"tunnels/tailscale/install POST"
|
||||
)
|
||||
);
|
||||
} finally {
|
||||
controller.close();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { startTailscaleLogin } from "@/lib/tailscaleTunnel";
|
||||
import { toPublicSafeTunnelError } from "@/lib/api/publicSafeTunnelError";
|
||||
import { parseOptionalJsonBody, requireTailscaleAuth, tailscaleLoginSchema } from "../routeUtils";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
@@ -16,9 +17,11 @@ export async function POST(request: Request) {
|
||||
return NextResponse.json(result);
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: error instanceof Error ? error.message : "Failed to start Tailscale login",
|
||||
},
|
||||
toPublicSafeTunnelError(
|
||||
error,
|
||||
"Failed to start the Tailscale login.",
|
||||
"tunnels/tailscale/login POST"
|
||||
),
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getTailscaleTunnelStatus } from "@/lib/tailscaleTunnel";
|
||||
import { toPublicSafeTunnelError } from "@/lib/api/publicSafeTunnelError";
|
||||
import { requireTailscaleAuth } from "./routeUtils";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
@@ -13,9 +14,11 @@ export async function GET(request: Request) {
|
||||
return NextResponse.json(status);
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: error instanceof Error ? error.message : "Failed to load Tailscale status",
|
||||
},
|
||||
toPublicSafeTunnelError(
|
||||
error,
|
||||
"Failed to load the Tailscale status.",
|
||||
"tunnels/tailscale GET"
|
||||
),
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { isAuthenticated } from "@/shared/utils/apiAuth";
|
||||
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
|
||||
import {
|
||||
formatValidationMessage,
|
||||
isValidationFailure,
|
||||
validateBody,
|
||||
} from "@/shared/validation/helpers";
|
||||
|
||||
export const tailscaleEnableSchema = z.object({
|
||||
sudoPassword: z.string().optional(),
|
||||
@@ -34,7 +38,15 @@ export async function parseOptionalJsonBody<T extends z.ZodTypeAny>(request: Req
|
||||
|
||||
const validation = validateBody(schema, rawBody);
|
||||
if (isValidationFailure(validation)) {
|
||||
return { response: validation.response };
|
||||
// validateBody() returns { success, error } — it has no `response` field, so
|
||||
// the previous `{ response: validation.response }` handed every caller an
|
||||
// `undefined` response and Next answered with a framework 500 instead of a 400.
|
||||
return {
|
||||
response: NextResponse.json(
|
||||
{ error: formatValidationMessage(validation.error) },
|
||||
{ status: 400 }
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
return { data: validation.data };
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getTailscaleTunnelStatus, startTailscaleDaemon } from "@/lib/tailscaleTunnel";
|
||||
import { toPublicSafeTunnelError } from "@/lib/api/publicSafeTunnelError";
|
||||
import { parseOptionalJsonBody, requireTailscaleAuth, tailscaleSudoSchema } from "../routeUtils";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
@@ -19,9 +20,11 @@ export async function POST(request: Request) {
|
||||
});
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: error instanceof Error ? error.message : "Failed to start the Tailscale daemon",
|
||||
},
|
||||
toPublicSafeTunnelError(
|
||||
error,
|
||||
"Failed to start the Tailscale daemon.",
|
||||
"tunnels/tailscale/start-daemon POST"
|
||||
),
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user