Gateway: reject published placeholder tokens

This commit is contained in:
Coy Geek
2026-04-17 20:44:03 -07:00
committed by Peter Steinberger
parent 960bc52e3c
commit 106b770c40
4 changed files with 80 additions and 54 deletions

View File

@@ -1,6 +1,7 @@
import fs from "node:fs/promises";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { KNOWN_WEAK_GATEWAY_TOKEN_PLACEHOLDERS } from "../gateway/known-weak-gateway-secrets.js";
const INSTALL_DOCS_DIR = path.join(process.cwd(), "docs", "install");
const CLOUD_INSTALL_DOCS = ["gcp.md", "hetzner.md"] as const;
@@ -10,7 +11,9 @@ describe("cloud install docs", () => {
for (const docName of CLOUD_INSTALL_DOCS) {
const markdown = await fs.readFile(path.join(INSTALL_DOCS_DIR, docName), "utf8");
expect(markdown).not.toContain("OPENCLAW_GATEWAY_TOKEN=change-me-now");
for (const token of KNOWN_WEAK_GATEWAY_TOKEN_PLACEHOLDERS) {
expect(markdown).not.toContain(`OPENCLAW_GATEWAY_TOKEN=${token}`);
}
expect(markdown).toMatch(/OPENCLAW_GATEWAY_TOKEN=[ \t]*\r?\n/);
expect(markdown).toContain("openssl rand -hex 32");
}

View File

@@ -0,0 +1,6 @@
export const KNOWN_WEAK_GATEWAY_TOKEN_PLACEHOLDERS = [
"change-me-to-a-long-random-token",
"change-me-now",
] as const;
export const KNOWN_WEAK_GATEWAY_PASSWORD_PLACEHOLDERS = ["change-me-to-a-strong-password"] as const;

View File

@@ -1,6 +1,7 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import { expectGeneratedTokenPersistedToGatewayAuth } from "../test-utils/auth-token-assertions.js";
import { KNOWN_WEAK_GATEWAY_TOKEN_PLACEHOLDERS } from "./known-weak-gateway-secrets.js";
import {
assertGatewayAuthNotKnownWeak,
assertHooksTokenSeparateFromGatewayAuth,
@@ -404,34 +405,40 @@ describe("ensureGatewayStartupAuth", () => {
).rejects.toThrow(/hooks\.token must not match gateway auth token/i);
});
it("rejects the .env.example placeholder token supplied via environment", async () => {
await expect(
ensureGatewayStartupAuth({
cfg: {},
env: {
OPENCLAW_GATEWAY_TOKEN: "change-me-to-a-long-random-token",
} as NodeJS.ProcessEnv,
}),
).rejects.toThrow(/example placeholder/i);
expect(mocks.replaceConfigFile).not.toHaveBeenCalled();
});
it.each(KNOWN_WEAK_GATEWAY_TOKEN_PLACEHOLDERS)(
"rejects the published placeholder token %s supplied via environment",
async (token) => {
await expect(
ensureGatewayStartupAuth({
cfg: {},
env: {
OPENCLAW_GATEWAY_TOKEN: token,
} as NodeJS.ProcessEnv,
}),
).rejects.toThrow(/example placeholder/i);
expect(mocks.replaceConfigFile).not.toHaveBeenCalled();
},
);
it("rejects the .env.example placeholder token supplied via config", async () => {
await expect(
ensureGatewayStartupAuth({
cfg: {
gateway: {
auth: {
mode: "token",
token: "change-me-to-a-long-random-token",
it.each(KNOWN_WEAK_GATEWAY_TOKEN_PLACEHOLDERS)(
"rejects the published placeholder token %s supplied via config",
async (token) => {
await expect(
ensureGatewayStartupAuth({
cfg: {
gateway: {
auth: {
mode: "token",
token,
},
},
},
},
env: {} as NodeJS.ProcessEnv,
}),
).rejects.toThrow(/example placeholder/i);
expect(mocks.replaceConfigFile).not.toHaveBeenCalled();
});
env: {} as NodeJS.ProcessEnv,
}),
).rejects.toThrow(/example placeholder/i);
expect(mocks.replaceConfigFile).not.toHaveBeenCalled();
},
);
it("rejects the .env.example placeholder password supplied via config", async () => {
await expect(
@@ -472,16 +479,19 @@ describe("assertGatewayAuthNotKnownWeak", () => {
mocks.replaceConfigFile.mockClear();
});
it("throws on the known-weak token sentinel", () => {
expect(() =>
assertGatewayAuthNotKnownWeak({
mode: "token",
modeSource: "config",
token: "change-me-to-a-long-random-token",
allowTailscale: false,
}),
).toThrow(/example placeholder/i);
});
it.each(KNOWN_WEAK_GATEWAY_TOKEN_PLACEHOLDERS)(
"throws on the known-weak token sentinel %s",
(token) => {
expect(() =>
assertGatewayAuthNotKnownWeak({
mode: "token",
modeSource: "config",
token,
allowTailscale: false,
}),
).toThrow(/example placeholder/i);
},
);
it("throws on the known-weak password sentinel", () => {
expect(() =>
@@ -494,16 +504,19 @@ describe("assertGatewayAuthNotKnownWeak", () => {
).toThrow(/example placeholder/i);
});
it("ignores whitespace-padded placeholder tokens (trimmed match)", () => {
expect(() =>
assertGatewayAuthNotKnownWeak({
mode: "token",
modeSource: "config",
token: " change-me-to-a-long-random-token ",
allowTailscale: false,
}),
).toThrow(/example placeholder/i);
});
it.each(KNOWN_WEAK_GATEWAY_TOKEN_PLACEHOLDERS)(
"rejects whitespace-padded placeholder token %s after trimming",
(token) => {
expect(() =>
assertGatewayAuthNotKnownWeak({
mode: "token",
modeSource: "config",
token: ` ${token} `,
allowTailscale: false,
}),
).toThrow(/example placeholder/i);
},
);
it("does not throw on an empty token (falls through to generation path)", () => {
expect(() =>

View File

@@ -18,6 +18,10 @@ import {
hasGatewayTokenEnvCandidate,
trimToUndefined,
} from "./credentials.js";
import {
KNOWN_WEAK_GATEWAY_PASSWORD_PLACEHOLDERS,
KNOWN_WEAK_GATEWAY_TOKEN_PLACEHOLDERS,
} from "./known-weak-gateway-secrets.js";
/**
* Placeholder credentials that have ever shipped in `.env.example` or been
@@ -31,13 +35,13 @@ import {
* the example file alone does not protect users who follow an older doc
* snippet or copy a tutorial command line.
*/
const KNOWN_WEAK_GATEWAY_TOKENS: ReadonlySet<string> = new Set([
"change-me-to-a-long-random-token",
]);
const KNOWN_WEAK_GATEWAY_TOKENS: ReadonlySet<string> = new Set(
KNOWN_WEAK_GATEWAY_TOKEN_PLACEHOLDERS,
);
const KNOWN_WEAK_GATEWAY_PASSWORDS: ReadonlySet<string> = new Set([
"change-me-to-a-strong-password", // pragma: allowlist secret
]);
const KNOWN_WEAK_GATEWAY_PASSWORDS: ReadonlySet<string> = new Set(
KNOWN_WEAK_GATEWAY_PASSWORD_PLACEHOLDERS,
);
export function mergeGatewayAuthConfig(
base?: GatewayAuthConfig,
@@ -269,8 +273,8 @@ export function assertGatewayAuthNotKnownWeak(auth: ResolvedGatewayAuth): void {
const token = auth.token?.trim() ?? "";
if (token && KNOWN_WEAK_GATEWAY_TOKENS.has(token)) {
throw new Error(
"Invalid config: gateway auth token is set to the example placeholder " +
"from .env.example. Generate a real secret (e.g. `openssl rand -hex 32`) " +
"Invalid config: gateway auth token is set to a published example placeholder " +
"from docs or .env.example. Generate a real secret (e.g. `openssl rand -hex 32`) " +
"and set OPENCLAW_GATEWAY_TOKEN or gateway.auth.token before starting " +
"the gateway.",
);