mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 02:01:25 +00:00
fix: make sensitive field whitelist case-insensitive (#16148)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: bb2d219e1f
Co-authored-by: akramcodez <179671552+akramcodez@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
This commit is contained in:
@@ -179,6 +179,29 @@ describe("redactConfigSnapshot", () => {
|
||||
expect(gw.auth.token).toBe(REDACTED_SENTINEL);
|
||||
});
|
||||
|
||||
it("does not redact passwordFile path fields", () => {
|
||||
const snapshot = makeSnapshot({
|
||||
channels: {
|
||||
irc: {
|
||||
passwordFile: "/etc/openclaw/irc-password.txt",
|
||||
nickserv: {
|
||||
passwordFile: "/etc/openclaw/nickserv-password.txt",
|
||||
password: "super-secret-nickserv-password",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = redactConfigSnapshot(snapshot);
|
||||
const channels = result.config.channels as Record<string, Record<string, unknown>>;
|
||||
const irc = channels.irc;
|
||||
const nickserv = irc.nickserv as Record<string, unknown>;
|
||||
|
||||
expect(irc.passwordFile).toBe("/etc/openclaw/irc-password.txt");
|
||||
expect(nickserv.passwordFile).toBe("/etc/openclaw/nickserv-password.txt");
|
||||
expect(nickserv.password).toBe(REDACTED_SENTINEL);
|
||||
});
|
||||
|
||||
it("preserves hash unchanged", () => {
|
||||
const snapshot = makeSnapshot({ gateway: { auth: { token: "secret-token-value-here" } } });
|
||||
const result = redactConfigSnapshot(snapshot);
|
||||
@@ -343,7 +366,9 @@ describe("redactConfigSnapshot", () => {
|
||||
});
|
||||
const result = redactConfigSnapshot(snapshot, hints);
|
||||
const custom = result.config.custom as Record<string, string>;
|
||||
const resolved = result.resolved as Record<string, Record<string, string>>;
|
||||
expect(custom.mySecret).toBe(REDACTED_SENTINEL);
|
||||
expect(resolved.custom.mySecret).toBe(REDACTED_SENTINEL);
|
||||
});
|
||||
|
||||
it("keeps regex fallback for extension keys not covered by uiHints", () => {
|
||||
@@ -630,7 +655,9 @@ describe("redactConfigSnapshot", () => {
|
||||
});
|
||||
const result = redactConfigSnapshot(snapshot, hints);
|
||||
const gw = result.config.gateway as Record<string, Record<string, string>>;
|
||||
const resolved = result.resolved as Record<string, Record<string, Record<string, string>>>;
|
||||
expect(gw.auth.token).toBe("not-actually-secret-value");
|
||||
expect(resolved.gateway.auth.token).toBe("not-actually-secret-value");
|
||||
});
|
||||
|
||||
it("does not redact paths absent from uiHints (schema is single source of truth)", () => {
|
||||
@@ -642,7 +669,9 @@ describe("redactConfigSnapshot", () => {
|
||||
});
|
||||
const result = redactConfigSnapshot(snapshot, hints);
|
||||
const gw = result.config.gateway as Record<string, Record<string, string>>;
|
||||
const resolved = result.resolved as Record<string, Record<string, Record<string, string>>>;
|
||||
expect(gw.auth.password).toBe("not-in-hints-value");
|
||||
expect(resolved.gateway.auth.password).toBe("not-in-hints-value");
|
||||
});
|
||||
|
||||
it("uses wildcard hints for array items", () => {
|
||||
|
||||
@@ -301,7 +301,7 @@ export function redactConfigSnapshot(
|
||||
const redactedRaw = snapshot.raw ? redactRawText(snapshot.raw, snapshot.config, uiHints) : null;
|
||||
const redactedParsed = snapshot.parsed ? redactObject(snapshot.parsed, uiHints) : snapshot.parsed;
|
||||
// Also redact the resolved config (contains values after ${ENV} substitution)
|
||||
const redactedResolved = redactConfigObject(snapshot.resolved);
|
||||
const redactedResolved = redactConfigObject(snapshot.resolved, uiHints);
|
||||
|
||||
return {
|
||||
...snapshot,
|
||||
|
||||
@@ -1,11 +1,38 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { z } from "zod";
|
||||
import { __test__ } from "./schema.hints.js";
|
||||
import { __test__, isSensitiveConfigPath } from "./schema.hints.js";
|
||||
import { OpenClawSchema } from "./zod-schema.js";
|
||||
import { sensitive } from "./zod-schema.sensitive.js";
|
||||
|
||||
const { mapSensitivePaths } = __test__;
|
||||
|
||||
describe("isSensitiveConfigPath", () => {
|
||||
it("matches whitelist suffixes case-insensitively", () => {
|
||||
const whitelistedPaths = [
|
||||
"maxTokens",
|
||||
"maxOutputTokens",
|
||||
"maxInputTokens",
|
||||
"maxCompletionTokens",
|
||||
"contextTokens",
|
||||
"totalTokens",
|
||||
"tokenCount",
|
||||
"tokenLimit",
|
||||
"tokenBudget",
|
||||
"channels.irc.nickserv.passwordFile",
|
||||
];
|
||||
for (const path of whitelistedPaths) {
|
||||
expect(isSensitiveConfigPath(path)).toBe(false);
|
||||
expect(isSensitiveConfigPath(path.toUpperCase())).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps true sensitive keys redacted", () => {
|
||||
expect(isSensitiveConfigPath("channels.slack.token")).toBe(true);
|
||||
expect(isSensitiveConfigPath("models.providers.openai.apiKey")).toBe(true);
|
||||
expect(isSensitiveConfigPath("channels.irc.nickserv.password")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("mapSensitivePaths", () => {
|
||||
it("should detect sensitive fields nested inside all structural Zod types", () => {
|
||||
const GrandSchema = z.object({
|
||||
|
||||
@@ -91,7 +91,7 @@ const FIELD_PLACEHOLDERS: Record<string, string> = {
|
||||
* These are explicitly excluded from redaction (plugin config) and
|
||||
* warnings about not being marked sensitive (base config).
|
||||
*/
|
||||
const SENSITIVE_KEY_WHITELIST = new Set([
|
||||
const SENSITIVE_KEY_WHITELIST_SUFFIXES = [
|
||||
"maxtokens",
|
||||
"maxoutputtokens",
|
||||
"maxinputtokens",
|
||||
@@ -102,15 +102,24 @@ const SENSITIVE_KEY_WHITELIST = new Set([
|
||||
"tokenlimit",
|
||||
"tokenbudget",
|
||||
"passwordFile",
|
||||
]);
|
||||
] as const;
|
||||
const NORMALIZED_SENSITIVE_KEY_WHITELIST_SUFFIXES = SENSITIVE_KEY_WHITELIST_SUFFIXES.map((suffix) =>
|
||||
suffix.toLowerCase(),
|
||||
);
|
||||
|
||||
const SENSITIVE_PATTERNS = [/token$/i, /password/i, /secret/i, /api.?key/i];
|
||||
|
||||
function isWhitelistedSensitivePath(path: string): boolean {
|
||||
const lowerPath = path.toLowerCase();
|
||||
return NORMALIZED_SENSITIVE_KEY_WHITELIST_SUFFIXES.some((suffix) => lowerPath.endsWith(suffix));
|
||||
}
|
||||
|
||||
function matchesSensitivePattern(path: string): boolean {
|
||||
return SENSITIVE_PATTERNS.some((pattern) => pattern.test(path));
|
||||
}
|
||||
|
||||
export function isSensitiveConfigPath(path: string): boolean {
|
||||
return (
|
||||
!Array.from(SENSITIVE_KEY_WHITELIST).some((suffix) => path.endsWith(suffix)) &&
|
||||
SENSITIVE_PATTERNS.some((pattern) => pattern.test(path))
|
||||
);
|
||||
return !isWhitelistedSensitivePath(path) && matchesSensitivePattern(path);
|
||||
}
|
||||
|
||||
export function buildBaseHints(): ConfigUiHints {
|
||||
|
||||
Reference in New Issue
Block a user