mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-03 13:52:09 +03:00
fix(authz): derive LOCAL_ONLY locality from Host header (middleware has no socket IP)
The authz pipeline runs in the Next middleware runtime (proxy.ts -> runAuthzPipeline) where ctx.request is a NextRequest with no .socket/.ip. requestPeerAddress therefore returned null, so isLoopbackRequest was ALWAYS false and every LOCAL_ONLY path 403'd even from loopback (Services/MCP/Traffic-Inspector were unusable). Read the Host header instead — exactly what isLoopbackHost/isPrivateLanHost were built to parse — which restores loopback and, combined with isPrivateLanHost, enables the owner-authorized private-LAN access. Spawn-capable endpoints still require manage-scope auth after this gate.
This commit is contained in:
@@ -19,7 +19,16 @@ import {
|
||||
const MODEL_SYNC_MANAGEMENT_PATH = /^\/api\/providers\/[^/]+\/(sync-models|models)$/;
|
||||
|
||||
function requestPeerAddress(ctx: PolicyContext): string | null {
|
||||
return ctx.request.ip || ctx.request.socket?.remoteAddress || null;
|
||||
// In the Next middleware runtime (proxy.ts → runAuthzPipeline), ctx.request is
|
||||
// a NextRequest with no socket/.ip, so the only locality signal is the Host
|
||||
// header — which is exactly what isLoopbackHost/isPrivateLanHost parse (they
|
||||
// strip :port). This both fixes the loopback gate (previously the null socket
|
||||
// made every LOCAL_ONLY request 403, even from localhost) and enables the
|
||||
// owner-authorized private-LAN carve-out. Fall back to .ip/.socket for any
|
||||
// non-middleware caller that provides them. Spawn-capable endpoints still
|
||||
// require manage-scope auth after this gate.
|
||||
const hostHeader = ctx.request.headers?.get?.("host") ?? null;
|
||||
return hostHeader || ctx.request.ip || ctx.request.socket?.remoteAddress || null;
|
||||
}
|
||||
|
||||
function isLoopbackRequest(ctx: PolicyContext): boolean {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { isPrivateLanHost, isLoopbackHost, isLocalOnlyPath } from "../../src/server/authz/routeGuard.ts";
|
||||
|
||||
test("isPrivateLanHost: accepts RFC1918 IPv4 (incl. :port and ::ffff: mapped)", () => {
|
||||
@@ -45,3 +47,11 @@ test("services + traffic-inspector remain LOCAL_ONLY paths", () => {
|
||||
assert.equal(isLocalOnlyPath("/api/services/9router/status"), true);
|
||||
assert.equal(isLocalOnlyPath("/api/tools/traffic-inspector/sessions"), true);
|
||||
});
|
||||
|
||||
test("management policy derives locality from the Host header (middleware socket is null)", () => {
|
||||
const src = readFileSync(
|
||||
join(import.meta.dirname, "../../src/server/authz/policies/management.ts"),
|
||||
"utf8"
|
||||
);
|
||||
assert.ok(src.includes('headers?.get?.("host")'), "requestPeerAddress must read the Host header");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user