mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 15:30:47 +00:00
fix(gateway): reject known-weak example auth credentials at startup (#64586)
This commit is contained in:
11
.env.example
11
.env.example
@@ -14,12 +14,15 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# Gateway auth + paths
|
||||
# -----------------------------------------------------------------------------
|
||||
# Recommended if the gateway binds beyond loopback.
|
||||
OPENCLAW_GATEWAY_TOKEN=change-me-to-a-long-random-token
|
||||
# Example generator: openssl rand -hex 32
|
||||
# Required if the gateway binds beyond loopback. Leave blank to have OpenClaw
|
||||
# auto-generate a token on first start, or provide your own using
|
||||
# `openssl rand -hex 32`. The gateway will refuse to start if this is set to
|
||||
# the documented example placeholder, so never copy-paste an example value
|
||||
# from docs or tutorials into this file verbatim.
|
||||
OPENCLAW_GATEWAY_TOKEN=
|
||||
|
||||
# Optional alternative auth mode (use token OR password).
|
||||
# OPENCLAW_GATEWAY_PASSWORD=change-me-to-a-strong-password
|
||||
# OPENCLAW_GATEWAY_PASSWORD=
|
||||
|
||||
# Optional path overrides (defaults shown for reference).
|
||||
# OPENCLAW_STATE_DIR=~/.openclaw
|
||||
|
||||
@@ -8,6 +8,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Gateway/auth: blank the shipped example gateway credential in `.env.example` and fail startup when a copied placeholder token or password is still configured, so operators cannot accidentally launch with a publicly known secret. (#64586) Thanks @navarrotech and @vincentkoc.
|
||||
- Memory/active-memory+dreaming: keep active-memory recall runs on the strongest resolved channel, consume managed dreaming heartbeat events exactly once, stop dreaming from re-ingesting its own narrative transcripts, and add explicit repair/dedupe recovery flows in CLI, doctor, and the Dreams UI.
|
||||
- Matrix/mentions: keep room mention gating strict while accepting visible `@displayName` Matrix URI labels, so `requireMention` works for non-OpenClaw Matrix clients again. (#64796) Thanks @hclsys.
|
||||
- Doctor: warn when on-disk agent directories still exist under `~/.openclaw/agents/<id>/agent` but the matching `agents.list[]` entries are missing from config. (#65113) Thanks @neeravmakwana.
|
||||
|
||||
@@ -14,13 +14,17 @@ vi.mock("../config/config.js", async () => {
|
||||
};
|
||||
});
|
||||
|
||||
let assertGatewayAuthNotKnownWeak: typeof import("./startup-auth.js").assertGatewayAuthNotKnownWeak;
|
||||
let assertHooksTokenSeparateFromGatewayAuth: typeof import("./startup-auth.js").assertHooksTokenSeparateFromGatewayAuth;
|
||||
let ensureGatewayStartupAuth: typeof import("./startup-auth.js").ensureGatewayStartupAuth;
|
||||
|
||||
async function loadFreshStartupAuthModuleForTest() {
|
||||
vi.resetModules();
|
||||
({ assertHooksTokenSeparateFromGatewayAuth, ensureGatewayStartupAuth } =
|
||||
await import("./startup-auth.js"));
|
||||
({
|
||||
assertGatewayAuthNotKnownWeak,
|
||||
assertHooksTokenSeparateFromGatewayAuth,
|
||||
ensureGatewayStartupAuth,
|
||||
} = await import("./startup-auth.js"));
|
||||
}
|
||||
|
||||
describe("ensureGatewayStartupAuth", () => {
|
||||
@@ -408,6 +412,138 @@ 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("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",
|
||||
},
|
||||
},
|
||||
},
|
||||
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(
|
||||
ensureGatewayStartupAuth({
|
||||
cfg: {
|
||||
gateway: {
|
||||
auth: {
|
||||
mode: "password",
|
||||
password: "change-me-to-a-strong-password", // pragma: allowlist secret
|
||||
},
|
||||
},
|
||||
},
|
||||
env: {} as NodeJS.ProcessEnv,
|
||||
}),
|
||||
).rejects.toThrow(/example placeholder/i);
|
||||
expect(mocks.replaceConfigFile).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("accepts any non-placeholder token (negative control)", async () => {
|
||||
await expectResolvedToken({
|
||||
cfg: {
|
||||
gateway: {
|
||||
auth: {
|
||||
mode: "token",
|
||||
token: "a-legit-random-token-0123456789abcdef",
|
||||
},
|
||||
},
|
||||
},
|
||||
env: {} as NodeJS.ProcessEnv,
|
||||
expectedToken: "a-legit-random-token-0123456789abcdef",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("assertGatewayAuthNotKnownWeak", () => {
|
||||
beforeEach(async () => {
|
||||
await loadFreshStartupAuthModuleForTest();
|
||||
});
|
||||
|
||||
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("throws on the known-weak password sentinel", () => {
|
||||
expect(() =>
|
||||
assertGatewayAuthNotKnownWeak({
|
||||
mode: "password",
|
||||
modeSource: "config",
|
||||
password: "change-me-to-a-strong-password", // pragma: allowlist secret
|
||||
allowTailscale: false,
|
||||
}),
|
||||
).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("does not throw on an empty token (falls through to generation path)", () => {
|
||||
expect(() =>
|
||||
assertGatewayAuthNotKnownWeak({
|
||||
mode: "token",
|
||||
modeSource: "config",
|
||||
token: "",
|
||||
allowTailscale: false,
|
||||
}),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it("does not throw on a real token", () => {
|
||||
expect(() =>
|
||||
assertGatewayAuthNotKnownWeak({
|
||||
mode: "token",
|
||||
modeSource: "config",
|
||||
token: "a-legit-random-token-0123456789abcdef",
|
||||
allowTailscale: false,
|
||||
}),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it("does not throw on the none mode", () => {
|
||||
expect(() =>
|
||||
assertGatewayAuthNotKnownWeak({
|
||||
mode: "none",
|
||||
modeSource: "default",
|
||||
allowTailscale: false,
|
||||
}),
|
||||
).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("assertHooksTokenSeparateFromGatewayAuth", () => {
|
||||
|
||||
@@ -19,6 +19,26 @@ import {
|
||||
trimToUndefined,
|
||||
} from "./credentials.js";
|
||||
|
||||
/**
|
||||
* Placeholder credentials that have ever shipped in `.env.example` or been
|
||||
* used as copy-paste examples in onboarding docs. If any of these ever
|
||||
* becomes the resolved gateway credential at startup, reject the launch —
|
||||
* the operator almost certainly copied an example file verbatim without
|
||||
* replacing the sentinel, which would otherwise leave the gateway protected
|
||||
* by a publicly-known credential.
|
||||
*
|
||||
* This is a belt-and-suspenders complement to keeping `.env.example` blank:
|
||||
* 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_PASSWORDS: ReadonlySet<string> = new Set([
|
||||
"change-me-to-a-strong-password", // pragma: allowlist secret
|
||||
]);
|
||||
|
||||
export function mergeGatewayAuthConfig(
|
||||
base?: GatewayAuthConfig,
|
||||
override?: GatewayAuthConfig,
|
||||
@@ -196,6 +216,7 @@ export async function ensureGatewayStartupAuth(params: {
|
||||
tailscaleOverride: params.tailscaleOverride,
|
||||
});
|
||||
if (resolved.mode !== "token" || (resolved.token?.trim().length ?? 0) > 0) {
|
||||
assertGatewayAuthNotKnownWeak(resolved);
|
||||
assertHooksTokenSeparateFromGatewayAuth({ cfg: params.cfg, auth: resolved });
|
||||
return { cfg: params.cfg, auth: resolved, persistedGeneratedToken: false };
|
||||
}
|
||||
@@ -229,6 +250,11 @@ export async function ensureGatewayStartupAuth(params: {
|
||||
authOverride: params.authOverride,
|
||||
tailscaleOverride: params.tailscaleOverride,
|
||||
});
|
||||
// The generated token is crypto-random, so this cannot match the weak set
|
||||
// in practice — but running the assertion on both branches documents that
|
||||
// the rule applies uniformly and guards against any future path that might
|
||||
// feed a non-generated value through nextAuth.
|
||||
assertGatewayAuthNotKnownWeak(nextAuth);
|
||||
assertHooksTokenSeparateFromGatewayAuth({ cfg: nextCfg, auth: nextAuth });
|
||||
return {
|
||||
cfg: nextCfg,
|
||||
@@ -238,6 +264,31 @@ export async function ensureGatewayStartupAuth(params: {
|
||||
};
|
||||
}
|
||||
|
||||
export function assertGatewayAuthNotKnownWeak(auth: ResolvedGatewayAuth): void {
|
||||
if (auth.mode === "token") {
|
||||
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`) " +
|
||||
"and set OPENCLAW_GATEWAY_TOKEN or gateway.auth.token before starting " +
|
||||
"the gateway.",
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (auth.mode === "password") {
|
||||
const password = auth.password?.trim() ?? "";
|
||||
if (password && KNOWN_WEAK_GATEWAY_PASSWORDS.has(password)) {
|
||||
throw new Error(
|
||||
"Invalid config: gateway auth password is set to the example placeholder " +
|
||||
"from .env.example. Choose a real password and set OPENCLAW_GATEWAY_PASSWORD " +
|
||||
"or gateway.auth.password before starting the gateway.",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function assertHooksTokenSeparateFromGatewayAuth(params: {
|
||||
cfg: OpenClawConfig;
|
||||
auth: ResolvedGatewayAuth;
|
||||
|
||||
Reference in New Issue
Block a user