Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions clients/web/server/resolve-bind-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@ export const BIND_ALL_INTERFACES_ENV = "DANGEROUSLY_BIND_ALL_INTERFACES";

/**
* An explicit, unambiguous opt-in. Unlike a bare `!!value` (which treats the
* string `"false"` as truthy), only `"true"`/`"1"` (case-insensitive) enable
* the override, so `DANGEROUSLY_BIND_ALL_INTERFACES=false` reads as "off".
* string `"false"` as truthy), only `"true"`/`"1"` (trimmed, case-insensitive)
* enable the override, so `DANGEROUSLY_BIND_ALL_INTERFACES=false` reads as
* "off". Exported so every `DANGEROUSLY_*` safety flag parses the same way —
* `DANGEROUSLY_OMIT_AUTH` once used `!!value`, and `=false` turned auth off
* (#2331). Anything unrecognized fails closed.
*/
function isEnabled(value: string | undefined): boolean {
export function isEnvFlagEnabled(value: string | undefined): boolean {
const v = value?.trim().toLowerCase();
return v === "true" || v === "1";
}
Expand Down Expand Up @@ -66,7 +69,10 @@ export function resolveBindHostname(
env: NodeJS.ProcessEnv = process.env,
): string {
const host = (env.HOST ?? DEFAULT_BIND_HOST).trim();
if (isAllInterfacesHost(host) && !isEnabled(env[BIND_ALL_INTERFACES_ENV])) {
if (
isAllInterfacesHost(host) &&
!isEnvFlagEnabled(env[BIND_ALL_INTERFACES_ENV])
) {
// Show the resolved address when it differs from the typed spelling — the
// guard now catches forms the resolver folds to the wildcard (a fullwidth
// `HOST="0"` renders like `0`, `HOST=0` / `0x0` / `::0` bind `0.0.0.0`), and
Expand Down
8 changes: 6 additions & 2 deletions clients/web/server/web-server-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { secretStorageSummary } from "../../../core/auth/secret-storage-info.ts"
import { readInspectorVersionSafe } from "../../../core/node/version.ts";
import { resolveSandboxPort } from "./sandbox-controller.js";
import { resolveAppOriginPort } from "./app-origin-controller.js";
import { resolveBindHostname } from "./resolve-bind-host.js";
import { isEnvFlagEnabled, resolveBindHostname } from "./resolve-bind-host.js";
import {
APP_ORIGIN_FULL_ADDRESS_ENV,
resolveAppOriginPublicOrigin,
Expand Down Expand Up @@ -386,7 +386,11 @@ export function buildWebServerConfig(
);
}
const hostname = resolveBindHostname();
const dangerouslyOmitAuth = !!process.env.DANGEROUSLY_OMIT_AUTH;
// Only an explicit `true`/`1` omits auth — `!!value` read `=false` as "on"
// and silently disabled the /api/* bearer gate (#2331).
const dangerouslyOmitAuth = isEnvFlagEnabled(
process.env.DANGEROUSLY_OMIT_AUTH,
);
const authToken = dangerouslyOmitAuth
? ""
: ((process.env[API_SERVER_ENV_VARS.AUTH_TOKEN] as string | undefined) ??
Expand Down
24 changes: 24 additions & 0 deletions clients/web/src/test/integration/server/web-server-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,30 @@ describe("buildWebServerConfigFromEnv", () => {
expect(cfg.authToken).toBe("");
});

// #2331: `!!value` read every non-empty string as "on", so a deployment that
// set DANGEROUSLY_OMIT_AUTH=false to keep auth on silently turned it off.
it.each(["true", "TRUE", " True ", "1", " 1 "])(
"omits auth for the explicit opt-in DANGEROUSLY_OMIT_AUTH=%j",
(value) => {
process.env.DANGEROUSLY_OMIT_AUTH = value;
process.env[API_SERVER_ENV_VARS.AUTH_TOKEN] = "ignored";
const cfg = buildWebServerConfigFromEnv();
expect(cfg.dangerouslyOmitAuth).toBe(true);
expect(cfg.authToken).toBe("");
},
);

it.each(["false", "FALSE", "0", "", " ", "no", "yes", "on", "2"])(
"keeps auth on for DANGEROUSLY_OMIT_AUTH=%j",
(value) => {
process.env.DANGEROUSLY_OMIT_AUTH = value;
process.env[API_SERVER_ENV_VARS.AUTH_TOKEN] = "kept";
const cfg = buildWebServerConfigFromEnv();
expect(cfg.dangerouslyOmitAuth).toBe(false);
expect(cfg.authToken).toBe("kept");
},
);

it("uses API_SERVER_ENV_VARS.AUTH_TOKEN when present", () => {
process.env[API_SERVER_ENV_VARS.AUTH_TOKEN] = "primary";
const cfg = buildWebServerConfigFromEnv();
Expand Down
2 changes: 1 addition & 1 deletion docs/environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ These guard the web backend, which spawns processes on request. Read [Host bindi
| --------------------------------- | -------- | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `MCP_INSPECTOR_API_TOKEN` | web, CLI | a random token per launch | Bearer token guarding every `/api/*` route (`x-mcp-remote-auth: Bearer <token>`). Set it to use a known token instead of the generated one printed in the launch banner. The CLI reads it only to fill the `autoConnect` parameter of the deep link it emits. |
| `MCP_PROXY_AUTH_TOKEN` | web | — | **Deprecated** v1 name for `MCP_INSPECTOR_API_TOKEN`, used only when the new name is unset. |
| `DANGEROUSLY_OMIT_AUTH` | web | unset | Disables the API token entirely. ⚠️ **Any non-empty value turns auth off, including `false` and `0`** — unset the variable to keep auth on. |
| `DANGEROUSLY_OMIT_AUTH` | web | unset | Disables the API token entirely when set to `true` or `1` (trimmed, case-insensitive). Any other value — including `false`, `0` and empty — keeps auth on. |
| `HOST` | web, CLI | `127.0.0.1` | Address the web server binds. An all-interfaces host (`0.0.0.0`, `::`, an empty string, and equivalent spellings) is **refused** unless `DANGEROUSLY_BIND_ALL_INTERFACES` is enabled. The CLI reads it only to build its deep link. |
| `DANGEROUSLY_BIND_ALL_INTERFACES` | web | off | Opts in to an all-interfaces `HOST`. Only `true` or `1` (case-insensitive) enable it, so `false` reads as off. The Docker image sets it. |
| `ALLOWED_ORIGINS` | web | derived from `HOST` | Comma-separated origins allowed to call the API. Unset, the list follows `HOST` at `CLIENT_PORT`: the loopback origins for a loopback host, the loopback origins plus `http://0.0.0.0` and `http://[::]` for an all-interfaces bind, and otherwise only the configured host's own origin (so binding a LAN address does **not** also allow `localhost`). **Replaces** the default list rather than adding to it, so list every form you browse from. Each entry must include the scheme (`http://localhost:6274`). The same list is the MCP Apps sandbox proxy's embedder allow-list (its `frame-ancestors` header and its referrer check), so a public Inspector origin must be listed here for the Apps tab to render. |
Expand Down
2 changes: 1 addition & 1 deletion docs/v1-to-v2-migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ This table maps v1 names to v2. Defaults, accepted values, and the variables wit
| `CLIENT_PORT` | same | Web UI port, default `6274`. Must be a fixed port — `0`/dynamic is rejected, since the origin allow-list and sandbox CSP derive from it |
| `HOST` | same, **guarded** | An all-interfaces host (`0.0.0.0`, `::`, and equivalent spellings) is now **refused** unless `DANGEROUSLY_BIND_ALL_INTERFACES=true`. Binding a specific IP or hostname needs no opt-in |
| `ALLOWED_ORIGINS` | same | Still comma-separated, still **replaces** the default list rather than merging. Entries must include the scheme |
| `DANGEROUSLY_OMIT_AUTH` | same | |
| `DANGEROUSLY_OMIT_AUTH` | same, **stricter** | Only `true` / `1` (trimmed, case-insensitive) disable auth now; v1 treated any non-empty value — even `false` — as on |
| `MCP_AUTO_OPEN_ENABLED` | same | Also governs opening the OAuth page in the CLI (`true` opens it even when stderr is not a TTY). The TUI does not read it |
| — | `DANGEROUSLY_BIND_ALL_INTERFACES` | New opt-in for a wildcard bind (the Docker image sets it) |
| — | `MCP_CATALOG_PATH` | Default catalog path |
Expand Down