mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-23 23:52:18 +03:00
feat(cli): boot exposure warning for unauthenticated LAN bind (GHSA-wmgv-ph3p-rv57)
The shipped default (bind 0.0.0.0 + no API-key requirement) is a deliberate, documented local-first posture — but an operator on an untrusted network should learn that at startup, not after a surprise quota bill. serve now prints a loud warning naming both escape hatches (REQUIRE_API_KEY=true or OMNIROUTE_SERVER_HOST=127.0.0.1) whenever the resolved bind is non-loopback and no key is required. Silent on loopback binds and when REQUIRE_API_KEY is enabled. The default posture itself is unchanged (operator decision).
This commit is contained in:
@@ -12,7 +12,7 @@ import {
|
||||
isFatalInstrumentationHookFailure,
|
||||
formatAndroidInstrumentationFailureHint,
|
||||
} from "../utils/ensureAndroidCacheDir.mjs";
|
||||
import { resolveServerHost } from "../utils/serverHost.mjs";
|
||||
import { resolveServerHost, resolveExposureWarning } from "../utils/serverHost.mjs";
|
||||
import {
|
||||
resolveMaxOldSpaceMb,
|
||||
calibrateHeapFallbackMb,
|
||||
@@ -132,6 +132,15 @@ export async function runServe(opts = {}) {
|
||||
`);
|
||||
}
|
||||
|
||||
// GHSA-wmgv-ph3p-rv57: the default posture (all interfaces + no API key) is a
|
||||
// deliberate local-first choice, but it must be loud at startup — an operator
|
||||
// on an untrusted network learns the two escape hatches here, not after a
|
||||
// surprise quota bill.
|
||||
const exposureWarning = resolveExposureWarning();
|
||||
if (exposureWarning) {
|
||||
console.warn(`\x1b[33m ⚠ ${exposureWarning}\x1b[0m\n`);
|
||||
}
|
||||
|
||||
const serverWsJs = join(APP_DIR, "server-ws.mjs");
|
||||
const serverJs = existsSync(serverWsJs) ? serverWsJs : join(APP_DIR, "server.js");
|
||||
|
||||
|
||||
@@ -24,3 +24,34 @@ export function resolveServerHost(
|
||||
}
|
||||
return "0.0.0.0";
|
||||
}
|
||||
|
||||
const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "::1", "[::1]"]);
|
||||
|
||||
/**
|
||||
* Boot-time exposure warning (GHSA-wmgv-ph3p-rv57): the shipped default binds
|
||||
* all interfaces while the inference plane requires no credentials, so any
|
||||
* LAN peer can spend the operator's quota. That local-first posture is a
|
||||
* deliberate, documented default — but it must be LOUD at startup so an
|
||||
* operator who never read the docs still learns the two escape hatches.
|
||||
*
|
||||
* Returns the warning text when the server will listen on a non-loopback
|
||||
* interface with no API-key requirement, or null when the exposure is closed.
|
||||
*
|
||||
* @param {NodeJS.ProcessEnv} [env]
|
||||
* @param {string} [host]
|
||||
* @returns {string | null}
|
||||
*/
|
||||
export function resolveExposureWarning(env = process.env, host = resolveServerHost(env)) {
|
||||
if (LOOPBACK_HOSTS.has(host)) return null;
|
||||
const requireKey = String(env.REQUIRE_API_KEY || "")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
if (requireKey === "true" || requireKey === "1" || requireKey === "yes") return null;
|
||||
return (
|
||||
`SECURITY: listening on ${host} with NO API-key requirement — the inference ` +
|
||||
`plane (/v1/*) is reachable by ANY device that can route to this host, and ` +
|
||||
`requests are billed to your configured providers. This local-first default ` +
|
||||
`is intentional, but on an untrusted network either set REQUIRE_API_KEY=true ` +
|
||||
`or bind loopback with OMNIROUTE_SERVER_HOST=127.0.0.1.`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { resolveServerHost } from "../../bin/cli/utils/serverHost.mjs";
|
||||
import { resolveServerHost, resolveExposureWarning } from "../../bin/cli/utils/serverHost.mjs";
|
||||
|
||||
test("serve hostname: Linux honors OMNIROUTE_SERVER_HOST when HOSTNAME is set", () => {
|
||||
assert.equal(
|
||||
@@ -55,3 +55,26 @@ test("serve hostname: Windows preserves an explicit legacy HOSTNAME", () => {
|
||||
test("serve hostname: Windows ignores an auto-set HOSTNAME matching the machine", () => {
|
||||
assert.equal(resolveServerHost({ HOSTNAME: "windows-pc" }, "win32", "windows-pc"), "0.0.0.0");
|
||||
});
|
||||
|
||||
test("exposure warning: fires when bound to all interfaces with no API-key requirement (GHSA-wmgv-ph3p-rv57)", () => {
|
||||
const warning = resolveExposureWarning({}, "0.0.0.0");
|
||||
assert.ok(warning, "a warning must be returned for the shipped default posture");
|
||||
assert.match(warning, /REQUIRE_API_KEY/);
|
||||
assert.match(warning, /OMNIROUTE_SERVER_HOST/);
|
||||
});
|
||||
|
||||
test("exposure warning: silent when REQUIRE_API_KEY is enabled", () => {
|
||||
assert.equal(resolveExposureWarning({ REQUIRE_API_KEY: "true" }, "0.0.0.0"), null);
|
||||
assert.equal(resolveExposureWarning({ REQUIRE_API_KEY: "1" }, "0.0.0.0"), null);
|
||||
});
|
||||
|
||||
test("exposure warning: silent on loopback binds", () => {
|
||||
assert.equal(resolveExposureWarning({}, "127.0.0.1"), null);
|
||||
assert.equal(resolveExposureWarning({}, "localhost"), null);
|
||||
assert.equal(resolveExposureWarning({}, "::1"), null);
|
||||
});
|
||||
|
||||
test("exposure warning: fires for a LAN bind too (any non-loopback interface)", () => {
|
||||
assert.ok(resolveExposureWarning({}, "192.168.0.17"));
|
||||
assert.ok(resolveExposureWarning({}, "::"));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user