From 59733a02c88415f90ead9b59c7f1036869fa22a2 Mon Sep 17 00:00:00 2001 From: Omair Afzal <32237905+omair445@users.noreply.github.com> Date: Fri, 13 Feb 2026 21:04:41 +0500 Subject: [PATCH] fix(configure): reject literal "undefined" and "null" gateway auth tokens (#13767) * fix(configure): reject literal "undefined" and "null" gateway auth tokens * fix(configure): reject literal "undefined" and "null" gateway auth tokens * fix(configure): validate gateway password prompt and harden token coercion (#13767) (thanks @omair445) * test: remove unused vitest imports in baseline lint fixtures (#13767) --------- Co-authored-by: Luna AI Co-authored-by: Peter Steinberger --- CHANGELOG.md | 1 + .../configure.gateway-auth.e2e.test.ts | 35 +++++++++++++++++++ src/commands/configure.gateway-auth.ts | 21 ++++++++--- src/commands/configure.gateway.ts | 9 +++-- src/commands/onboard-helpers.e2e.test.ts | 29 +++++++++++++++ src/commands/onboard-helpers.ts | 22 +++++++++++- ...rovider-usage.auth.normalizes-keys.test.ts | 2 +- src/plugins/discovery.test.ts | 2 +- src/wizard/onboarding.gateway-config.ts | 8 +++-- 9 files changed, 118 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57e956c4df5..c75c275dec7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,6 +73,7 @@ Docs: https://docs.openclaw.ai - Gateway: drain active turns before restart to prevent message loss. (#13931) Thanks @0xRaini. - Gateway: auto-generate auth token during install to prevent launchd restart loops. (#13813) Thanks @cathrynlavery. - Gateway: prevent `undefined`/missing token in auth config. (#13809) Thanks @asklee-klawd. +- Configure/Gateway: reject literal `"undefined"`/`"null"` token input and validate gateway password prompt values to avoid invalid password-mode configs. (#13767) Thanks @omair445. - Gateway: handle async `EPIPE` on stdout/stderr during shutdown. (#13414) Thanks @keshav55. - Gateway/Control UI: resolve missing dashboard assets when `openclaw` is installed globally via symlink-based Node managers (nvm/fnm/n/Homebrew). (#14919) Thanks @aynorica. - Cron: use requested `agentId` for isolated job auth resolution. (#13983) Thanks @0xRaini. diff --git a/src/commands/configure.gateway-auth.e2e.test.ts b/src/commands/configure.gateway-auth.e2e.test.ts index be7fe347a53..ff9e32c31c8 100644 --- a/src/commands/configure.gateway-auth.e2e.test.ts +++ b/src/commands/configure.gateway-auth.e2e.test.ts @@ -44,6 +44,15 @@ describe("buildGatewayAuthConfig", () => { expect(result).toEqual({ mode: "password", password: "secret" }); }); + it("does not silently omit password when literal string is provided", () => { + const result = buildGatewayAuthConfig({ + mode: "password", + password: "undefined", + }); + + expect(result).toEqual({ mode: "password", password: "undefined" }); + }); + it("generates random token when token param is undefined", () => { const result = buildGatewayAuthConfig({ mode: "token", @@ -82,4 +91,30 @@ describe("buildGatewayAuthConfig", () => { expect(typeof result?.token).toBe("string"); expect(result?.token?.length).toBeGreaterThan(0); }); + + it('generates random token when token param is the literal string "undefined"', () => { + const result = buildGatewayAuthConfig({ + mode: "token", + token: "undefined", + }); + + expect(result?.mode).toBe("token"); + expect(result?.token).toBeDefined(); + expect(result?.token).not.toBe("undefined"); + expect(typeof result?.token).toBe("string"); + expect(result?.token?.length).toBeGreaterThan(0); + }); + + it('generates random token when token param is the literal string "null"', () => { + const result = buildGatewayAuthConfig({ + mode: "token", + token: "null", + }); + + expect(result?.mode).toBe("token"); + expect(result?.token).toBeDefined(); + expect(result?.token).not.toBe("null"); + expect(typeof result?.token).toBe("string"); + expect(result?.token?.length).toBeGreaterThan(0); + }); }); diff --git a/src/commands/configure.gateway-auth.ts b/src/commands/configure.gateway-auth.ts index 7a5d6f098fe..d7260401202 100644 --- a/src/commands/configure.gateway-auth.ts +++ b/src/commands/configure.gateway-auth.ts @@ -16,6 +16,18 @@ import { randomToken } from "./onboard-helpers.js"; type GatewayAuthChoice = "token" | "password"; +/** Reject undefined, empty, and common JS string-coercion artifacts for token auth. */ +function sanitizeTokenValue(value: string | undefined): string | undefined { + if (typeof value !== "string") { + return undefined; + } + const trimmed = value.trim(); + if (!trimmed || trimmed === "undefined" || trimmed === "null") { + return undefined; + } + return trimmed; +} + const ANTHROPIC_OAUTH_MODEL_KEYS = [ "anthropic/claude-opus-4-6", "anthropic/claude-opus-4-5", @@ -36,11 +48,12 @@ export function buildGatewayAuthConfig(params: { } if (params.mode === "token") { - // Guard against undefined/empty token to prevent JSON.stringify from writing the string "undefined" - const safeToken = params.token?.trim() || randomToken(); - return { ...base, mode: "token", token: safeToken }; + // Keep token mode always valid: treat empty/undefined/"undefined"/"null" as missing and generate a token. + const token = sanitizeTokenValue(params.token) ?? randomToken(); + return { ...base, mode: "token", token }; } - return { ...base, mode: "password", password: params.password }; + const password = params.password?.trim(); + return { ...base, mode: "password", ...(password && { password }) }; } export async function promptAuthConfig( diff --git a/src/commands/configure.gateway.ts b/src/commands/configure.gateway.ts index 1432b81d765..5e4f279b741 100644 --- a/src/commands/configure.gateway.ts +++ b/src/commands/configure.gateway.ts @@ -5,7 +5,12 @@ import { findTailscaleBinary } from "../infra/tailscale.js"; import { note } from "../terminal/note.js"; import { buildGatewayAuthConfig } from "./configure.gateway-auth.js"; import { confirm, select, text } from "./configure.shared.js"; -import { guardCancel, normalizeGatewayTokenInput, randomToken } from "./onboard-helpers.js"; +import { + guardCancel, + normalizeGatewayTokenInput, + randomToken, + validateGatewayPasswordInput, +} from "./onboard-helpers.js"; type GatewayAuthChoice = "token" | "password"; @@ -189,7 +194,7 @@ export async function promptGatewayConfig( const password = guardCancel( await text({ message: "Gateway password", - validate: (value) => (value?.trim() ? undefined : "Required"), + validate: validateGatewayPasswordInput, }), runtime, ); diff --git a/src/commands/onboard-helpers.e2e.test.ts b/src/commands/onboard-helpers.e2e.test.ts index d351c21527a..f0d7a184352 100644 --- a/src/commands/onboard-helpers.e2e.test.ts +++ b/src/commands/onboard-helpers.e2e.test.ts @@ -4,6 +4,7 @@ import { openUrl, resolveBrowserOpenCommand, resolveControlUiLinks, + validateGatewayPasswordInput, } from "./onboard-helpers.js"; const mocks = vi.hoisted(() => ({ @@ -121,4 +122,32 @@ describe("normalizeGatewayTokenInput", () => { it("returns empty string for non-string input", () => { expect(normalizeGatewayTokenInput(123)).toBe(""); }); + + it('rejects the literal string "undefined"', () => { + expect(normalizeGatewayTokenInput("undefined")).toBe(""); + }); + + it('rejects the literal string "null"', () => { + expect(normalizeGatewayTokenInput("null")).toBe(""); + }); +}); + +describe("validateGatewayPasswordInput", () => { + it("requires a non-empty password", () => { + expect(validateGatewayPasswordInput("")).toBe("Required"); + expect(validateGatewayPasswordInput(" ")).toBe("Required"); + }); + + it("rejects literal string coercion artifacts", () => { + expect(validateGatewayPasswordInput("undefined")).toBe( + 'Cannot be the literal string "undefined" or "null"', + ); + expect(validateGatewayPasswordInput("null")).toBe( + 'Cannot be the literal string "undefined" or "null"', + ); + }); + + it("accepts a normal password", () => { + expect(validateGatewayPasswordInput(" secret ")).toBeUndefined(); + }); }); diff --git a/src/commands/onboard-helpers.ts b/src/commands/onboard-helpers.ts index 08691203534..f1c40d3b79b 100644 --- a/src/commands/onboard-helpers.ts +++ b/src/commands/onboard-helpers.ts @@ -73,7 +73,27 @@ export function normalizeGatewayTokenInput(value: unknown): string { if (typeof value !== "string") { return ""; } - return value.trim(); + const trimmed = value.trim(); + // Reject the literal string "undefined" — a common bug when JS undefined + // gets coerced to a string via template literals or String(undefined). + if (trimmed === "undefined" || trimmed === "null") { + return ""; + } + return trimmed; +} + +export function validateGatewayPasswordInput(value: unknown): string | undefined { + if (typeof value !== "string") { + return "Required"; + } + const trimmed = value.trim(); + if (!trimmed) { + return "Required"; + } + if (trimmed === "undefined" || trimmed === "null") { + return 'Cannot be the literal string "undefined" or "null"'; + } + return undefined; } export function printWizardHeader(runtime: RuntimeEnv) { diff --git a/src/infra/provider-usage.auth.normalizes-keys.test.ts b/src/infra/provider-usage.auth.normalizes-keys.test.ts index 079f9014a62..5b193061ecd 100644 --- a/src/infra/provider-usage.auth.normalizes-keys.test.ts +++ b/src/infra/provider-usage.auth.normalizes-keys.test.ts @@ -1,6 +1,6 @@ import fs from "node:fs/promises"; import path from "node:path"; -import { describe, expect, it, vi } from "vitest"; +import { describe, expect, it } from "vitest"; import { withTempHome } from "../../test/helpers/temp-home.js"; import { resolveProviderAuths } from "./provider-usage.auth.js"; diff --git a/src/plugins/discovery.test.ts b/src/plugins/discovery.test.ts index fd286ea8c22..3d19b02b17a 100644 --- a/src/plugins/discovery.test.ts +++ b/src/plugins/discovery.test.ts @@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, it } from "vitest"; import { discoverOpenClawPlugins } from "./discovery.js"; const tempDirs: string[] = []; diff --git a/src/wizard/onboarding.gateway-config.ts b/src/wizard/onboarding.gateway-config.ts index fa3b8be2e90..e7ecac2b6d2 100644 --- a/src/wizard/onboarding.gateway-config.ts +++ b/src/wizard/onboarding.gateway-config.ts @@ -7,7 +7,11 @@ import type { WizardFlow, } from "./onboarding.types.js"; import type { WizardPrompter } from "./prompts.js"; -import { normalizeGatewayTokenInput, randomToken } from "../commands/onboard-helpers.js"; +import { + normalizeGatewayTokenInput, + randomToken, + validateGatewayPasswordInput, +} from "../commands/onboard-helpers.js"; import { findTailscaleBinary } from "../infra/tailscale.js"; // These commands are "high risk" (privacy writes/recording) and should be @@ -208,7 +212,7 @@ export async function configureGatewayForOnboarding( ? quickstartGateway.password : await prompter.text({ message: "Gateway password", - validate: (value) => (value?.trim() ? undefined : "Required"), + validate: validateGatewayPasswordInput, }); nextConfig = { ...nextConfig,