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 <luna@coredirection.ai> Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
parent
4dc93f40d5
commit
59733a02c8
@ -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.
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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,
|
||||
);
|
||||
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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[] = [];
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user