fix(config): redact dynamic catchall secret keys

This commit is contained in:
Peter Steinberger
2026-02-24 00:21:19 +00:00
parent 8dfa33d373
commit f0c3c8b6a3
3 changed files with 58 additions and 4 deletions

View File

@@ -22,6 +22,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Security/Config: redact sensitive-looking dynamic catchall keys in `config.get` snapshots (for example `env.*` and `skills.entries.*.env.*`) and preserve round-trip restore behavior for those redacted sentinels. Thanks @merc1305.
- Tests/Vitest: tier local parallel worker defaults by host memory, keep gateway serial by default on non-high-memory hosts, and document a low-profile fallback command for memory-constrained land/gate runs to prevent local OOMs. (#24719) Thanks @ngutman.
- Agents/Context pruning: extend `cache-ttl` eligibility to Moonshot/Kimi and ZAI/GLM providers (including OpenRouter model refs), so `contextPruning.mode: "cache-ttl"` is no longer silently skipped for those sessions. (#24497) Thanks @lailoo.
- Tools/web_search: add `provider: "kimi"` (Moonshot) support with key/config schema wiring and a corrected two-step `$web_search` tool flow that echoes tool results before final synthesis, including citation extraction from search results. (#16616, #18822) Thanks @adshine.

View File

@@ -656,7 +656,7 @@ describe("redactConfigSnapshot", () => {
expectGatewayAuthFieldValue(result, "token", "not-actually-secret-value");
});
it("does not redact paths absent from uiHints (schema is single source of truth)", () => {
it("redacts sensitive-looking paths even when absent from uiHints (defense in depth)", () => {
const hints: ConfigUiHints = {
"some.other.path": { sensitive: true },
};
@@ -664,7 +664,57 @@ describe("redactConfigSnapshot", () => {
gateway: { auth: { password: "not-in-hints-value" } },
});
const result = redactConfigSnapshot(snapshot, hints);
expectGatewayAuthFieldValue(result, "password", "not-in-hints-value");
expectGatewayAuthFieldValue(result, "password", REDACTED_SENTINEL);
});
it("redacts and restores dynamic env catchall secrets when uiHints miss the path", () => {
const hints: ConfigUiHints = {
"some.other.path": { sensitive: true },
};
const snapshot = makeSnapshot({
env: {
GROQ_API_KEY: "gsk-secret-123",
NODE_ENV: "production",
},
});
const redacted = redactConfigSnapshot(snapshot, hints);
const env = redacted.config.env as Record<string, string>;
expect(env.GROQ_API_KEY).toBe(REDACTED_SENTINEL);
expect(env.NODE_ENV).toBe("production");
const restored = restoreRedactedValues(redacted.config, snapshot.config, hints);
expect(restored.env.GROQ_API_KEY).toBe("gsk-secret-123");
expect(restored.env.NODE_ENV).toBe("production");
});
it("redacts and restores skills entry env secrets in dynamic record paths", () => {
const hints: ConfigUiHints = {
"some.other.path": { sensitive: true },
};
const snapshot = makeSnapshot({
skills: {
entries: {
web_search: {
env: {
GEMINI_API_KEY: "gemini-secret-456",
BRAVE_REGION: "us",
},
},
},
},
});
const redacted = redactConfigSnapshot(snapshot, hints);
const entry = (
redacted.config.skills as {
entries: Record<string, { env: Record<string, string> }>;
}
).entries.web_search;
expect(entry.env.GEMINI_API_KEY).toBe(REDACTED_SENTINEL);
expect(entry.env.BRAVE_REGION).toBe("us");
const restored = restoreRedactedValues(redacted.config, snapshot.config, hints);
expect(restored.skills.entries.web_search.env.GEMINI_API_KEY).toBe("gemini-secret-456");
expect(restored.skills.entries.web_search.env.BRAVE_REGION).toBe("us");
});
it("uses wildcard hints for array items", () => {

View File

@@ -164,7 +164,10 @@ function redactObjectWithLookup(
break;
}
}
if (!matched && isExtensionPath(path)) {
if (!matched) {
// Fall back to pattern-based guessing for paths not covered by schema
// hints. This catches dynamic keys inside catchall objects (for example
// env.GROQ_API_KEY) and extension/plugin config alike.
const markedNonSensitive = isExplicitlyNonSensitivePath(hints, [path, wildcardPath]);
if (
typeof value === "string" &&
@@ -542,7 +545,7 @@ function restoreRedactedValuesWithLookup(
break;
}
}
if (!matched && isExtensionPath(path)) {
if (!matched) {
const markedNonSensitive = isExplicitlyNonSensitivePath(hints, [path, wildcardPath]);
if (!markedNonSensitive && isSensitivePath(path) && value === REDACTED_SENTINEL) {
result[key] = restoreOriginalValueOrThrow({ key, path, original: orig });