mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:30:43 +00:00
fix: harden diagnostics support redaction
This commit is contained in:
@@ -25,7 +25,7 @@ Docs: https://docs.openclaw.ai
|
||||
- TUI: add local embedded mode for running terminal chats without a Gateway while keeping plugin approval gates enforced. (#66767) Thanks @fuller-stack-dev.
|
||||
- CLI/Claude: default `claude-cli` runs to warm stdio sessions, including custom configs that omit transport fields, and resume from the stored Claude session after Gateway restarts or idle exits. (#69679) Thanks @obviyus.
|
||||
- Control UI/settings+chat: add a browser-local personal identity for the operator (name plus local-safe avatar), route user identity rendering through the shared chat/avatar path used by assistant and agent surfaces, and tighten Quick Settings, agent fallback chips, and narrow-screen chat layouts so personalization no longer wastes space or clips controls. (#70362) Thanks @BunsDev.
|
||||
- Gateway/diagnostics: add a support-ready diagnostics export with sanitized logs, status, health, config, and stability snapshots for bug reports. (#70324) Thanks @gumadeiras.
|
||||
- Gateway/diagnostics: enable payload-free stability recording by default and add a support-ready diagnostics export with sanitized logs, status, health, config, and stability snapshots for bug reports. (#70324) Thanks @gumadeiras.
|
||||
|
||||
### Fixes
|
||||
|
||||
|
||||
@@ -135,7 +135,7 @@ Options:
|
||||
Notes:
|
||||
|
||||
- The recorder is active by default. Set `diagnostics.enabled: false` only when you need to disable Gateway diagnostic heartbeat collection.
|
||||
- Records keep operational metadata: event names, counts, byte sizes, memory readings, queue/session state, channel/plugin names, and session ids. They do not keep chat text, webhook bodies, tool outputs, raw request or response bodies, tokens, cookies, or secret values.
|
||||
- Records keep operational metadata: event names, counts, byte sizes, memory readings, queue/session state, channel/plugin names, and redacted session summaries. They do not keep chat text, webhook bodies, tool outputs, raw request or response bodies, tokens, cookies, secret values, hostnames, or raw session ids.
|
||||
- On fatal Gateway exits, shutdown timeouts, and restart startup failures, OpenClaw writes the same diagnostic snapshot to `~/.openclaw/logs/stability/openclaw-stability-*.json` when the recorder has events. Inspect the newest bundle with `openclaw gateway stability --bundle latest`; `--limit`, `--type`, and `--since-seq` also apply to bundle output.
|
||||
|
||||
### `gateway diagnostics export`
|
||||
|
||||
@@ -81,6 +81,9 @@ describe("diagnostic stability bundles", () => {
|
||||
name: "Error",
|
||||
code: "ERR_TEST",
|
||||
},
|
||||
host: {
|
||||
hostname: "<redacted-hostname>",
|
||||
},
|
||||
snapshot: {
|
||||
count: 2,
|
||||
},
|
||||
@@ -94,6 +97,7 @@ describe("diagnostic stability bundles", () => {
|
||||
expect(raw).not.toContain("chat-secret");
|
||||
expect(raw).not.toContain("message body");
|
||||
expect(raw).not.toContain("contains secret message");
|
||||
expect(raw).not.toContain(os.hostname());
|
||||
});
|
||||
|
||||
it("skips empty recorder snapshots by default", () => {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
import { resolveStateDir } from "../config/paths.js";
|
||||
@@ -17,6 +16,7 @@ export const DEFAULT_DIAGNOSTIC_STABILITY_BUNDLE_RETENTION = 20;
|
||||
const SAFE_REASON_CODE = /^[A-Za-z0-9_.:-]{1,120}$/u;
|
||||
const BUNDLE_PREFIX = "openclaw-stability-";
|
||||
const BUNDLE_SUFFIX = ".json";
|
||||
const REDACTED_HOSTNAME = "<redacted-hostname>";
|
||||
|
||||
export type DiagnosticStabilityBundle = {
|
||||
version: typeof DIAGNOSTIC_STABILITY_BUNDLE_VERSION;
|
||||
@@ -294,7 +294,7 @@ export function writeDiagnosticStabilityBundleSync(
|
||||
uptimeMs: Math.round(process.uptime() * 1000),
|
||||
},
|
||||
host: {
|
||||
hostname: os.hostname(),
|
||||
hostname: REDACTED_HOSTNAME,
|
||||
},
|
||||
...(error ? { error } : {}),
|
||||
snapshot,
|
||||
|
||||
@@ -48,6 +48,12 @@ describe("diagnostic support export", () => {
|
||||
|
||||
it("writes a shareable zip without raw chats, webhook bodies, or secrets", async () => {
|
||||
const fakeToken = "sk-test-support-export-secret-token-1234567890";
|
||||
const fakeAwsKey = ["AKIA", "IOSFODNN7EXAMPLE"].join("");
|
||||
const fakeJwt = [
|
||||
"eyJhbGciOiJIUzI1NiIs",
|
||||
"eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4i",
|
||||
"SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c",
|
||||
].join(".");
|
||||
const privateChat = "private user said diagnose my bank transfer";
|
||||
const webhookBody = "raw webhook body with message contents";
|
||||
const credentialUrl =
|
||||
@@ -122,7 +128,7 @@ describe("diagnostic support export", () => {
|
||||
subsystem: "gateway",
|
||||
component: "gateway/server",
|
||||
channel: "telegram",
|
||||
msg: `gateway websocket listening at ${credentialUrl}`,
|
||||
msg: `gateway websocket listening at ${credentialUrl} Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== ${fakeAwsKey} ${fakeJwt} Cookie: sid=secret`,
|
||||
hostname: "support-host",
|
||||
message: privateChat,
|
||||
body: webhookBody,
|
||||
@@ -237,9 +243,15 @@ describe("diagnostic support export", () => {
|
||||
expect(combined).not.toContain("short-token");
|
||||
expect(combined).not.toContain(tempDir);
|
||||
expect(combined).not.toContain("cron/jobs.json");
|
||||
expect(combined).not.toContain(os.hostname());
|
||||
expect(combined).not.toContain("QWxhZGRpbjpvcGVuIHNlc2FtZQ==");
|
||||
expect(combined).not.toContain("sid=secret");
|
||||
expect(combined).not.toContain(fakeAwsKey);
|
||||
expect(combined).not.toContain(fakeJwt);
|
||||
expect(combined).toContain("payload.large");
|
||||
expect(combined).toContain("gateway.http.json");
|
||||
expect(combined).toContain("$OPENCLAW_STATE_DIR");
|
||||
expect(combined).toContain("<redacted-hostname>");
|
||||
expect(combined).toContain("gateway-status.json");
|
||||
expect(combined).toContain("gateway-health.json");
|
||||
expect(combined).toContain("Attach this zip to the bug report");
|
||||
@@ -252,6 +264,10 @@ describe("diagnostic support export", () => {
|
||||
expect(sanitizedLogs).toContain(
|
||||
"wss://<redacted>:<redacted>@gateway.example/ws?token=<redacted>",
|
||||
);
|
||||
expect(sanitizedLogs).toContain("Basic <redacted>");
|
||||
expect(sanitizedLogs).toContain("Cookie: <redacted>");
|
||||
expect(sanitizedLogs).toContain("<redacted-aws-key>");
|
||||
expect(sanitizedLogs).toContain("<redacted-jwt>");
|
||||
expect(sanitizedLogs).toContain('"module":"matrix-auto-reply"');
|
||||
expect(sanitizedLogs).toContain('"subsystem":"gateway/channels/matrix"');
|
||||
expect(sanitizedLogs).toContain('"logger":"gateway-runtime"');
|
||||
@@ -350,11 +366,21 @@ describe("diagnostic support export", () => {
|
||||
});
|
||||
|
||||
it("redacts support text identifiers without hiding useful URL hosts", () => {
|
||||
const fakeAwsKey = ["ASIA", "IOSFODNN7EXAMPLE"].join("");
|
||||
const fakeJwt = [
|
||||
"eyJhbGciOiJIUzI1NiIs",
|
||||
"eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4i",
|
||||
"SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c",
|
||||
].join(".");
|
||||
const cases = [
|
||||
[
|
||||
"connect wss://support-user:support-password@gateway.example/ws?token=short-token&ok=1",
|
||||
"connect wss://<redacted>:<redacted>@gateway.example/ws?token=<redacted>",
|
||||
],
|
||||
["auth Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==", "auth Basic <redacted>"],
|
||||
["Cookie: sid=secret; theme=light", "Cookie: <redacted>"],
|
||||
[`aws ${fakeAwsKey}`, "aws <redacted-aws-key>"],
|
||||
[`jwt ${fakeJwt}`, "jwt <redacted-jwt>"],
|
||||
["email alice@example.com", "email <redacted-email>"],
|
||||
["matrix @support-user:matrix.example.com", "matrix <redacted-matrix-user>"],
|
||||
["room !support-room:matrix.example.com", "room <redacted-matrix-room>"],
|
||||
|
||||
@@ -12,6 +12,10 @@ const CONFIG_PRIVATE_FIELD_RE =
|
||||
/(?:allow[-_]?from|allow[-_]?to|deny[-_]?from|deny[-_]?to|blocked[-_]?from|blocked[-_]?users|owner[-_]?id|sender[-_]?id|recipient[-_]?id)/iu;
|
||||
const SENSITIVE_COMMAND_ARG_RE =
|
||||
/^--(?:api[-_]?key|hook[-_]?token|password|password-file|passwd|secret|token)(?:=.*)?$/iu;
|
||||
const BASIC_AUTH_RE = /\bBasic\s+[A-Za-z0-9+/]+={0,2}/giu;
|
||||
const COOKIE_HEADER_RE = /\b(Cookie|Set-Cookie)\s*:\s*[^\r\n]+/giu;
|
||||
const AWS_ACCESS_KEY_ID_RE = /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/gu;
|
||||
const JWT_RE = /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/gu;
|
||||
const URL_USERINFO_RE = /\b([a-z][a-z0-9+.-]*:\/\/)([^/@\s:?#]+):([^/@\s?#]+)@/giu;
|
||||
const SENSITIVE_URL_PARAM_RE =
|
||||
/([?&](?:api[-_]?key|access[-_]?token|auth[-_]?token|hook[-_]?token|password|passwd|refresh[-_]?token|secret|token)=)[^&#\s]+/giu;
|
||||
@@ -115,6 +119,7 @@ function redactKnownPathPrefixesForSupport(
|
||||
|
||||
export function redactTextForSupport(value: string): string {
|
||||
let redacted = redactSensitiveTextForSupport(value);
|
||||
redacted = redactCommonCredentialTextForSupport(redacted);
|
||||
redacted = redactUrlSecretsForSupport(redacted);
|
||||
redacted = redactServiceIdentifiersForSupport(redacted);
|
||||
redacted = redactContactIdentifiersForSupport(redacted);
|
||||
@@ -125,6 +130,14 @@ function redactSensitiveTextForSupport(value: string): string {
|
||||
return redactSensitiveText(value, { mode: "tools" });
|
||||
}
|
||||
|
||||
function redactCommonCredentialTextForSupport(value: string): string {
|
||||
return value
|
||||
.replace(BASIC_AUTH_RE, "Basic <redacted>")
|
||||
.replace(COOKIE_HEADER_RE, "$1: <redacted>")
|
||||
.replace(AWS_ACCESS_KEY_ID_RE, "<redacted-aws-key>")
|
||||
.replace(JWT_RE, "<redacted-jwt>");
|
||||
}
|
||||
|
||||
function redactUrlSecretsForSupport(value: string): string {
|
||||
return value
|
||||
.replace(URL_USERINFO_RE, "$1<redacted>:<redacted>@")
|
||||
|
||||
Reference in New Issue
Block a user