mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 19:14:33 +00:00
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:
@@ -73,6 +73,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
- Gateway: drain active turns before restart to prevent message loss. (#13931) Thanks @0xRaini.
|
- 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: 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.
|
- 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: 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.
|
- 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.
|
- 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" });
|
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", () => {
|
it("generates random token when token param is undefined", () => {
|
||||||
const result = buildGatewayAuthConfig({
|
const result = buildGatewayAuthConfig({
|
||||||
mode: "token",
|
mode: "token",
|
||||||
@@ -82,4 +91,30 @@ describe("buildGatewayAuthConfig", () => {
|
|||||||
expect(typeof result?.token).toBe("string");
|
expect(typeof result?.token).toBe("string");
|
||||||
expect(result?.token?.length).toBeGreaterThan(0);
|
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";
|
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 = [
|
const ANTHROPIC_OAUTH_MODEL_KEYS = [
|
||||||
"anthropic/claude-opus-4-6",
|
"anthropic/claude-opus-4-6",
|
||||||
"anthropic/claude-opus-4-5",
|
"anthropic/claude-opus-4-5",
|
||||||
@@ -36,11 +48,12 @@ export function buildGatewayAuthConfig(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (params.mode === "token") {
|
if (params.mode === "token") {
|
||||||
// Guard against undefined/empty token to prevent JSON.stringify from writing the string "undefined"
|
// Keep token mode always valid: treat empty/undefined/"undefined"/"null" as missing and generate a token.
|
||||||
const safeToken = params.token?.trim() || randomToken();
|
const token = sanitizeTokenValue(params.token) ?? randomToken();
|
||||||
return { ...base, mode: "token", token: safeToken };
|
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(
|
export async function promptAuthConfig(
|
||||||
|
|||||||
@@ -5,7 +5,12 @@ import { findTailscaleBinary } from "../infra/tailscale.js";
|
|||||||
import { note } from "../terminal/note.js";
|
import { note } from "../terminal/note.js";
|
||||||
import { buildGatewayAuthConfig } from "./configure.gateway-auth.js";
|
import { buildGatewayAuthConfig } from "./configure.gateway-auth.js";
|
||||||
import { confirm, select, text } from "./configure.shared.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";
|
type GatewayAuthChoice = "token" | "password";
|
||||||
|
|
||||||
@@ -189,7 +194,7 @@ export async function promptGatewayConfig(
|
|||||||
const password = guardCancel(
|
const password = guardCancel(
|
||||||
await text({
|
await text({
|
||||||
message: "Gateway password",
|
message: "Gateway password",
|
||||||
validate: (value) => (value?.trim() ? undefined : "Required"),
|
validate: validateGatewayPasswordInput,
|
||||||
}),
|
}),
|
||||||
runtime,
|
runtime,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
openUrl,
|
openUrl,
|
||||||
resolveBrowserOpenCommand,
|
resolveBrowserOpenCommand,
|
||||||
resolveControlUiLinks,
|
resolveControlUiLinks,
|
||||||
|
validateGatewayPasswordInput,
|
||||||
} from "./onboard-helpers.js";
|
} from "./onboard-helpers.js";
|
||||||
|
|
||||||
const mocks = vi.hoisted(() => ({
|
const mocks = vi.hoisted(() => ({
|
||||||
@@ -121,4 +122,32 @@ describe("normalizeGatewayTokenInput", () => {
|
|||||||
it("returns empty string for non-string input", () => {
|
it("returns empty string for non-string input", () => {
|
||||||
expect(normalizeGatewayTokenInput(123)).toBe("");
|
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") {
|
if (typeof value !== "string") {
|
||||||
return "";
|
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) {
|
export function printWizardHeader(runtime: RuntimeEnv) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import path from "node:path";
|
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 { withTempHome } from "../../test/helpers/temp-home.js";
|
||||||
import { resolveProviderAuths } from "./provider-usage.auth.js";
|
import { resolveProviderAuths } from "./provider-usage.auth.js";
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
|
|||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
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";
|
import { discoverOpenClawPlugins } from "./discovery.js";
|
||||||
|
|
||||||
const tempDirs: string[] = [];
|
const tempDirs: string[] = [];
|
||||||
|
|||||||
@@ -7,7 +7,11 @@ import type {
|
|||||||
WizardFlow,
|
WizardFlow,
|
||||||
} from "./onboarding.types.js";
|
} from "./onboarding.types.js";
|
||||||
import type { WizardPrompter } from "./prompts.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";
|
import { findTailscaleBinary } from "../infra/tailscale.js";
|
||||||
|
|
||||||
// These commands are "high risk" (privacy writes/recording) and should be
|
// These commands are "high risk" (privacy writes/recording) and should be
|
||||||
@@ -208,7 +212,7 @@ export async function configureGatewayForOnboarding(
|
|||||||
? quickstartGateway.password
|
? quickstartGateway.password
|
||||||
: await prompter.text({
|
: await prompter.text({
|
||||||
message: "Gateway password",
|
message: "Gateway password",
|
||||||
validate: (value) => (value?.trim() ? undefined : "Required"),
|
validate: validateGatewayPasswordInput,
|
||||||
});
|
});
|
||||||
nextConfig = {
|
nextConfig = {
|
||||||
...nextConfig,
|
...nextConfig,
|
||||||
|
|||||||
Reference in New Issue
Block a user