mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:50:43 +00:00
fix(dashboard): keep bearer token out of runtime logs
Avoid logging tokenized Control UI URLs or SSH hints while preserving clipboard/browser token handoff.\n\nThanks @Ziy1-Tan!
This commit is contained in:
@@ -71,6 +71,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Dashboard/security: avoid writing tokenized Control UI URLs or SSH hints to runtime logs, keeping gateway bearer fragments out of console-captured logs readable through `logs.tail`. (#70029) Thanks @Ziy1-Tan.
|
||||
- Discord/cron: deliver text-only isolated cron and heartbeat announce output from the canonical final assistant text once, avoiding duplicate Discord posts when streamed block payloads and the final answer contain the same content. Fixes #71406. Thanks @alexgross21.
|
||||
- macOS Gateway: wait for launchd to reload the exited Gateway LaunchAgent before bootstrapping repair fallback, preventing config-triggered restarts from leaving the service not loaded. Fixes #45178. Thanks @vincentkoc.
|
||||
- TTS/hooks: preserve audio-only TTS transcripts for `message_sending` and `message_sent` hooks without rendering the transcript as a media caption. Thanks @zqchris.
|
||||
|
||||
@@ -89,6 +89,7 @@ describe("dashboardCommand", () => {
|
||||
customBindHost: undefined,
|
||||
basePath: undefined,
|
||||
});
|
||||
// clipboard and browser still get the full authenticated URL
|
||||
expect(copyToClipboardMock).toHaveBeenCalledWith("http://127.0.0.1:18789/#token=abc123");
|
||||
expect(openUrlMock).toHaveBeenCalledWith("http://127.0.0.1:18789/#token=abc123");
|
||||
expect(runtime.log).toHaveBeenCalledWith(
|
||||
@@ -96,6 +97,34 @@ describe("dashboardCommand", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("never logs the gateway token in the dashboard URL (CVE regression)", async () => {
|
||||
const secretToken = "super-secret-bearer-token";
|
||||
mockSnapshot(secretToken);
|
||||
copyToClipboardMock.mockResolvedValue(true);
|
||||
detectBrowserOpenSupportMock.mockResolvedValue({ ok: true });
|
||||
openUrlMock.mockResolvedValue(true);
|
||||
|
||||
await dashboardCommand(runtime);
|
||||
|
||||
// Clipboard and browser should still receive the tokenized URL.
|
||||
expect(copyToClipboardMock).toHaveBeenCalledWith(
|
||||
`http://127.0.0.1:18789/#token=${secretToken}`,
|
||||
);
|
||||
expect(openUrlMock).toHaveBeenCalledWith(`http://127.0.0.1:18789/#token=${secretToken}`);
|
||||
|
||||
// The logged output must never contain the token — it flows into
|
||||
// console-captured log files readable by operator.read-scoped devices.
|
||||
for (const call of runtime.log.mock.calls) {
|
||||
const line = String(call[0]);
|
||||
expect(line).not.toContain(secretToken);
|
||||
expect(line).not.toContain("#token=");
|
||||
}
|
||||
|
||||
// Base URL should be logged without the fragment.
|
||||
expect(runtime.log).toHaveBeenCalledWith("Dashboard URL: http://127.0.0.1:18789/");
|
||||
expect(runtime.log).toHaveBeenCalledWith("Token auto-auth included in browser/clipboard URL.");
|
||||
});
|
||||
|
||||
it("prints SSH hint when browser cannot open", async () => {
|
||||
mockSnapshot("shhhh");
|
||||
copyToClipboardMock.mockResolvedValue(false);
|
||||
@@ -111,14 +140,50 @@ describe("dashboardCommand", () => {
|
||||
expect(runtime.log).toHaveBeenCalledWith("ssh hint");
|
||||
});
|
||||
|
||||
it("respects --no-open and skips browser attempts", async () => {
|
||||
mockSnapshot();
|
||||
it("never passes token to SSH hint (CVE regression — SSH path)", async () => {
|
||||
const secretToken = "super-secret-bearer-token";
|
||||
mockSnapshot(secretToken);
|
||||
copyToClipboardMock.mockResolvedValue(false);
|
||||
detectBrowserOpenSupportMock.mockResolvedValue({ ok: false, reason: "ssh" });
|
||||
formatControlUiSshHintMock.mockReturnValue("ssh hint without token");
|
||||
|
||||
await dashboardCommand(runtime);
|
||||
|
||||
// formatControlUiSshHint must NOT receive the token — the returned
|
||||
// hint string is written to runtime.log, which flows into the same
|
||||
// console-captured log file readable by operator.read-scoped devices.
|
||||
expect(formatControlUiSshHintMock).toHaveBeenCalledWith({ port: 18789, basePath: undefined });
|
||||
expect(formatControlUiSshHintMock).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({ token: expect.anything() }),
|
||||
);
|
||||
|
||||
// Double-check: no logged line contains the secret.
|
||||
for (const call of runtime.log.mock.calls) {
|
||||
const line = String(call[0]);
|
||||
expect(line).not.toContain(secretToken);
|
||||
expect(line).not.toContain("#token=");
|
||||
}
|
||||
});
|
||||
|
||||
it("respects --no-open and tells user token URL is in clipboard", async () => {
|
||||
mockSnapshot("abc");
|
||||
copyToClipboardMock.mockResolvedValue(true);
|
||||
|
||||
await dashboardCommand(runtime, { noOpen: true });
|
||||
|
||||
expect(detectBrowserOpenSupportMock).not.toHaveBeenCalled();
|
||||
expect(openUrlMock).not.toHaveBeenCalled();
|
||||
expect(runtime.log).toHaveBeenCalledWith(
|
||||
"Browser launch disabled (--no-open). Token-authenticated URL copied to clipboard.",
|
||||
);
|
||||
});
|
||||
|
||||
it("respects --no-open with plain URL hint when clipboard fails", async () => {
|
||||
mockSnapshot("abc");
|
||||
copyToClipboardMock.mockResolvedValue(false);
|
||||
|
||||
await dashboardCommand(runtime, { noOpen: true });
|
||||
|
||||
expect(runtime.log).toHaveBeenCalledWith(
|
||||
"Browser launch disabled (--no-open). Use the URL above.",
|
||||
);
|
||||
|
||||
@@ -46,7 +46,10 @@ export async function dashboardCommand(
|
||||
? `${links.httpUrl}#token=${encodeURIComponent(token)}`
|
||||
: links.httpUrl;
|
||||
|
||||
runtime.log(`Dashboard URL: ${dashboardUrl}`);
|
||||
runtime.log(`Dashboard URL: ${links.httpUrl}`);
|
||||
if (includeTokenInUrl) {
|
||||
runtime.log("Token auto-auth included in browser/clipboard URL.");
|
||||
}
|
||||
if (resolvedToken.secretRefConfigured && token) {
|
||||
runtime.log(
|
||||
"Token auto-auth is disabled for SecretRef-managed gateway.auth.token; use your external token source if prompted.",
|
||||
@@ -73,11 +76,13 @@ export async function dashboardCommand(
|
||||
hint = formatControlUiSshHint({
|
||||
port,
|
||||
basePath,
|
||||
token: includeTokenInUrl ? token || undefined : undefined,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
hint = "Browser launch disabled (--no-open). Use the URL above.";
|
||||
hint =
|
||||
copied && includeTokenInUrl
|
||||
? "Browser launch disabled (--no-open). Token-authenticated URL copied to clipboard."
|
||||
: "Browser launch disabled (--no-open). Use the URL above.";
|
||||
}
|
||||
|
||||
if (opened) {
|
||||
|
||||
Reference in New Issue
Block a user