mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
fix(security): use trusted internal origin for provider auto-sync self-fetch (CodeQL #323 SSRF) (#3336)
POST /api/providers fires a credential-bearing self-fetch to the new connection's /sync-models route (forwarding the management cookie + internal sync auth headers). #3267 built that origin from new URL(request.url).origin — the client-controlled Host header — so a (management-authenticated) caller could redirect the internal request to an arbitrary host, exfiltrating the internal sync auth token (CodeQL js/request-forgery, critical, alert #323). Derive the origin from the trusted loopback/env-pinned base URL via a new getModelSyncInternalBaseUrl() helper (same source the model-sync scheduler already uses), never from the incoming request. Adds a regression test.
This commit is contained in:
committed by
GitHub
parent
9535fa52a6
commit
df2379053e
@@ -32,7 +32,10 @@ import {
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
import { isManagedProviderConnectionId } from "@/lib/providers/catalog";
|
||||
import { isApiKeyRevealEnabled, maskStoredApiKey } from "@/lib/apiKeyExposure";
|
||||
import { buildModelSyncInternalHeaders } from "@/shared/services/modelSyncScheduler";
|
||||
import {
|
||||
buildModelSyncInternalHeaders,
|
||||
getModelSyncInternalBaseUrl,
|
||||
} from "@/shared/services/modelSyncScheduler";
|
||||
|
||||
// GET /api/providers - List all connections
|
||||
export async function GET(request: Request) {
|
||||
@@ -174,7 +177,12 @@ export async function POST(request: Request) {
|
||||
// auth header (defense in depth) and an X-Internal-Auto-Sync marker for
|
||||
// log correlation.
|
||||
try {
|
||||
const internalOrigin = new URL(request.url).origin;
|
||||
// SECURITY: use the trusted loopback/env-pinned origin, NOT
|
||||
// `new URL(request.url).origin` — the latter comes from the client-
|
||||
// controlled Host header, which would let a caller redirect this
|
||||
// credential-bearing internal self-fetch to an arbitrary host
|
||||
// (SSRF + internal-auth-header exfiltration; CodeQL js/request-forgery).
|
||||
const internalOrigin = getModelSyncInternalBaseUrl();
|
||||
const cookieHeader = request.headers.get("cookie") || "";
|
||||
const syncHeaders: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
|
||||
@@ -24,6 +24,19 @@ const INTERNAL_BASE_URL =
|
||||
process.env.NEXT_PUBLIC_APP_URL ||
|
||||
`http://127.0.0.1:${dashboardPort}`;
|
||||
|
||||
/**
|
||||
* Trusted origin for server-internal self-fetches (model sync, auto-discovery).
|
||||
*
|
||||
* SECURITY: never derive this from the incoming request (`request.url` /
|
||||
* `Host` header) — that is client-controlled and lets a caller redirect an
|
||||
* internal, credential-bearing self-fetch to an arbitrary host (SSRF +
|
||||
* internal-auth-header exfiltration; CodeQL js/request-forgery). Always use
|
||||
* this loopback/env-pinned origin instead.
|
||||
*/
|
||||
export function getModelSyncInternalBaseUrl(): string {
|
||||
return INTERNAL_BASE_URL;
|
||||
}
|
||||
|
||||
const globalState = globalThis as typeof globalThis & {
|
||||
__omnirouteModelSyncInternalAuthToken?: string;
|
||||
};
|
||||
|
||||
35
tests/unit/providers-autosync-ssrf-323.test.ts
Normal file
35
tests/unit/providers-autosync-ssrf-323.test.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Regression guard — CodeQL js/request-forgery alert #323 (v3.8.13).
|
||||
*
|
||||
* POST /api/providers fires a non-blocking self-fetch to the connection's
|
||||
* /sync-models route, forwarding the management cookie + internal sync auth
|
||||
* headers. #3267 built that self-fetch origin from `new URL(request.url).origin`
|
||||
* — i.e. the client-controlled Host header — so a caller could redirect the
|
||||
* credential-bearing internal request to an arbitrary host (SSRF + internal
|
||||
* auth-header exfiltration).
|
||||
*
|
||||
* The origin must come from the trusted loopback/env-pinned base URL
|
||||
* (`getModelSyncInternalBaseUrl()`), never from the incoming request.
|
||||
*/
|
||||
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
const routeSrc = readFileSync(
|
||||
join(import.meta.dirname, "../../src/app/api/providers/route.ts"),
|
||||
"utf8"
|
||||
);
|
||||
|
||||
test("POST /api/providers auto-sync uses the trusted internal origin (not request.url) — #323", () => {
|
||||
assert.ok(
|
||||
routeSrc.includes("getModelSyncInternalBaseUrl()"),
|
||||
"auto-sync self-fetch must derive its origin from getModelSyncInternalBaseUrl()"
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
routeSrc,
|
||||
/const\s+internalOrigin\s*=\s*new URL\(request\.url\)\.origin/,
|
||||
"auto-sync origin must NOT be derived from the client-controlled request.url/Host (SSRF, CodeQL js/request-forgery #323)"
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user