mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-15 03:12:36 +03:00
docker-compose.yml and docker-compose.prod.yml defaulted API_HOST/LIVE_WS_HOST/ HOSTNAME to 0.0.0.0 and published the dashboard/API/live-WS ports with bare, unscoped specs, which Docker expands to every interface. Combined with REQUIRE_API_KEY=false shipping as the .env.example default, this exposed the anonymous /v1 LLM proxy to the whole LAN/WAN (#12568). The optional cliproxyapi sidecar had the same unscoped publish spec plus no forwarded auth env var, exposing a credential-bearing service the same way (#12578); qdrant and bifrost had the identical gap. Applies the existing Redis loopback-bind precedent (tests/unit/compose-redis- loopback-bind.test.ts) to the app's own ports and to cliproxyapi/qdrant/bifrost: - New APP_BIND_HOST / CLIPROXY_BIND_HOST / QDRANT_BIND_HOST / BIFROST_BIND_HOST opt-in vars, defaulting to 127.0.0.1, documented in .env.example and docs/reference/ENVIRONMENT.md. - API_HOST/LIVE_WS_HOST default to 127.0.0.1 in both compose files; the prod file no longer hardcodes HOSTNAME=0.0.0.0. - cliproxyapi now forwards CLIPROXYAPI_MANAGEMENT_KEY as MANAGEMENT_PASSWORD, the one env var the pinned image actually reads for its management API. - A new boot-time guard (src/lib/startup/nonLoopbackApiKeyGuard.ts) logs a warning — never a hard failure — when the API bridge or live-WS server ends up bound to a non-loopback host while REQUIRE_API_KEY is disabled. ⚠️ base-red inherited: #12732 — unit #12058, integration codex-cache, package-artifact, tarball-smoke, agent-skills-sync Closes #12568 Closes #12578
77 lines
2.6 KiB
TypeScript
77 lines
2.6 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import { warnIfNonLoopbackWithoutApiKey } from "@/lib/startup/nonLoopbackApiKeyGuard";
|
|
|
|
// #12568: docker-compose can bind the app's ports to a non-loopback interface
|
|
// while REQUIRE_API_KEY still defaults to false, exposing the anonymous /v1
|
|
// proxy to the LAN/WAN. This guard warns (never blocks) when that combination
|
|
// is detected at server startup.
|
|
|
|
function withEnv<T>(vars: Record<string, string | undefined>, fn: () => T): T {
|
|
const prev: Record<string, string | undefined> = {};
|
|
for (const key of Object.keys(vars)) {
|
|
prev[key] = process.env[key];
|
|
const value = vars[key];
|
|
if (value === undefined) delete process.env[key];
|
|
else process.env[key] = value;
|
|
}
|
|
try {
|
|
return fn();
|
|
} finally {
|
|
for (const key of Object.keys(prev)) {
|
|
const value = prev[key];
|
|
if (value === undefined) delete process.env[key];
|
|
else process.env[key] = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
function captureWarn(fn: () => void): string[] {
|
|
const messages: string[] = [];
|
|
const original = console.warn;
|
|
console.warn = (...args: unknown[]) => {
|
|
messages.push(args.map(String).join(" "));
|
|
};
|
|
try {
|
|
fn();
|
|
} finally {
|
|
console.warn = original;
|
|
}
|
|
return messages;
|
|
}
|
|
|
|
test("warns when bound to 0.0.0.0 with REQUIRE_API_KEY unset (default false)", () => {
|
|
withEnv({ REQUIRE_API_KEY: undefined }, () => {
|
|
const messages = captureWarn(() => warnIfNonLoopbackWithoutApiKey("Test server", "0.0.0.0"));
|
|
assert.equal(messages.length, 1);
|
|
assert.match(messages[0], /non-loopback host "0\.0\.0\.0"/);
|
|
assert.match(messages[0], /REQUIRE_API_KEY/);
|
|
});
|
|
});
|
|
|
|
test("warns when bound to a LAN IP with REQUIRE_API_KEY=false", () => {
|
|
withEnv({ REQUIRE_API_KEY: "false" }, () => {
|
|
const messages = captureWarn(() => warnIfNonLoopbackWithoutApiKey("Test server", "192.168.1.5"));
|
|
assert.equal(messages.length, 1);
|
|
});
|
|
});
|
|
|
|
test("stays silent when bound to loopback regardless of REQUIRE_API_KEY", () => {
|
|
withEnv({ REQUIRE_API_KEY: "false" }, () => {
|
|
const messages = captureWarn(() => warnIfNonLoopbackWithoutApiKey("Test server", "127.0.0.1"));
|
|
assert.equal(messages.length, 0);
|
|
});
|
|
withEnv({ REQUIRE_API_KEY: "false" }, () => {
|
|
const messages = captureWarn(() => warnIfNonLoopbackWithoutApiKey("Test server", "::1"));
|
|
assert.equal(messages.length, 0);
|
|
});
|
|
});
|
|
|
|
test("stays silent when bound to 0.0.0.0 with REQUIRE_API_KEY=true", () => {
|
|
withEnv({ REQUIRE_API_KEY: "true" }, () => {
|
|
const messages = captureWarn(() => warnIfNonLoopbackWithoutApiKey("Test server", "0.0.0.0"));
|
|
assert.equal(messages.length, 0);
|
|
});
|
|
});
|