fix: redact sensitive CLI argv from config-audit.jsonl#60871
fix: redact sensitive CLI argv from config-audit.jsonl#60871lml2468 wants to merge 1 commit intoopenclaw:mainfrom
Conversation
The config tamper detection audit log was recording full CLI argv including plaintext gateway tokens, bot tokens, and API keys. Now strips/redacts known secret patterns from argv before writing to config-audit.jsonl to prevent credential exposure at rest. Added redactArgv() helper function and SENSITIVE_ARGV_FLAGS set that covers: --token, --bot-token, --app-token, --access-token, --gateway-token, --password, --api-key, --secret, --secret-key, --secret-input. Both --flag value and --flag=value forms are handled. Fixes openclaw#60826
|
Closing this PR because the author has more than 10 active PRs in this repo. Please reduce the active PR queue and reopen or resubmit once it is back under the limit. You can close your own PRs to get back under the limit. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b1967e7f5e
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| * be redacted before writing to config-audit.jsonl. | ||
| * Fixes openclaw/openclaw#60826. | ||
| */ | ||
| const SENSITIVE_ARGV_FLAGS = new Set([ |
There was a problem hiding this comment.
Include all secret CLI flags in argv redaction
The new redaction relies on a hardcoded allowlist, but several existing secret-bearing CLI flags are not included, so their values still get written in plaintext to config-audit.jsonl. For example, onboarding defines --custom-api-key, --gateway-password, and --remote-token (src/cli/program/register.onboard.ts:110-127), and webhooks defines --hook-token/--push-token (src/cli/webhooks-cli.ts:44-45); none are covered by SENSITIVE_ARGV_FLAGS, so redactArgv() leaves them unmasked. This means the credential leak path remains open for those command paths.
Useful? React with 👍 / 👎.
Greptile SummaryThis PR fixes a real security issue: the config tamper-detection audit log ( Key points:
Confidence Score: 4/5Safe to merge — the fix correctly addresses the credential-leak vector, but the absence of unit tests for the security-critical helper is a notable gap worth resolving. The core logic is sound and applied consistently at all three audit write sites. No regressions are introduced — the only behavioral change is that known sensitive flag values are replaced with [REDACTED] in the log. Score is 4 rather than 5 because redactArgv() has no unit tests covering its edge cases (flag as last element, --flag=value form, chained flags), and the redact-before-slice ordering is undocumented, creating a subtle latent risk in future refactors. src/config/io.ts — specifically the redactArgv() function (lines 114–136) and the three call sites; test coverage would reduce risk significantly. Prompt To Fix All With AIThis is a comment left during a code review.
Path: src/config/io.ts
Line: 128-131
Comment:
**Sensitive flag as the last argument is not redacted**
When a sensitive flag appears as the very last element of `argv` (no following value), the guard `i + 1 < argv.length` is `false`, so the code falls through to `out.push(arg)` — the flag name is written to the audit log without any indication that it may have carried a value in a different invocation. This is not a credential leak by itself (no value is present), but it means the logic silently does nothing for the malformed/truncated case rather than treating it uniformly.
More importantly, because `.slice(0, 8)` is applied to the redacted array **after** redaction, consider the following scenario:
```
process.argv = ["node", "openclaw", "a", "b", "c", "d", "e", "--token", "secretval", ...]
// idx: 0 1 2 3 4 5 6 7 8
```
`redactArgv()` runs on the **full** array. At index 7 (`--token`), `i + 1 = 8 < len` is true, so both `--token` and `[REDACTED]` are pushed and `i` advances to 8 — the result array still has `--token` at index 7 and `[REDACTED]` at index 8. After `.slice(0, 8)` the entry at index 8 is cut off, leaving `--token` without `[REDACTED]`. No value is exposed (the slice removed it either way), but the flag name is still present in the record.
This is safe against leaking values, but it may be worth adding a comment near the `slice(0, 8)` call sites to document that redaction must run before slicing (to avoid accidentally re-ordering them in a future refactor):
```suggestion
if (SENSITIVE_ARGV_FLAGS.has(arg) && i + 1 < argv.length) {
out.push(arg, "[REDACTED]");
i++;
continue;
}
// Note: if a sensitive flag is the last element with no following value,
// there is nothing to redact — pass it through unchanged.
```
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: src/config/io.ts
Line: 114-136
Comment:
**No unit tests for security-critical `redactArgv` helper**
`redactArgv()` is an untested security helper guarding against credential leaks in the audit log. The existing test files (`io.write-config.test.ts`, `io.observe-config.test.ts`) contain no coverage of the argv redaction behavior.
Given the security sensitivity and the two distinct code paths (`--flag value` and `--flag=value`), this function is a strong candidate for unit tests. Because it is currently module-private it cannot be tested in isolation — it either needs to be exported for a colocated test or covered via the full audit-log write path.
Suggested cases worth covering:
- `--flag value` form is redacted
- `--flag=value` form is redacted
- Non-sensitive flags pass through unchanged
- Sensitive flag as the last element (no following value) does not crash
- Chained sensitive flags are all independently redacted
How can I resolve this? If you propose a fix, please make it concise.Reviews (1): Last reviewed commit: "fix: redact sensitive CLI argv from conf..." | Re-trigger Greptile |
| if (SENSITIVE_ARGV_FLAGS.has(arg) && i + 1 < argv.length) { | ||
| out.push(arg, "[REDACTED]"); | ||
| i++; | ||
| continue; |
There was a problem hiding this comment.
Sensitive flag as the last argument is not redacted
When a sensitive flag appears as the very last element of argv (no following value), the guard i + 1 < argv.length is false, so the code falls through to out.push(arg) — the flag name is written to the audit log without any indication that it may have carried a value in a different invocation. This is not a credential leak by itself (no value is present), but it means the logic silently does nothing for the malformed/truncated case rather than treating it uniformly.
More importantly, because .slice(0, 8) is applied to the redacted array after redaction, consider the following scenario:
process.argv = ["node", "openclaw", "a", "b", "c", "d", "e", "--token", "secretval", ...]
// idx: 0 1 2 3 4 5 6 7 8
redactArgv() runs on the full array. At index 7 (--token), i + 1 = 8 < len is true, so both --token and [REDACTED] are pushed and i advances to 8 — the result array still has --token at index 7 and [REDACTED] at index 8. After .slice(0, 8) the entry at index 8 is cut off, leaving --token without [REDACTED]. No value is exposed (the slice removed it either way), but the flag name is still present in the record.
This is safe against leaking values, but it may be worth adding a comment near the slice(0, 8) call sites to document that redaction must run before slicing (to avoid accidentally re-ordering them in a future refactor):
| if (SENSITIVE_ARGV_FLAGS.has(arg) && i + 1 < argv.length) { | |
| out.push(arg, "[REDACTED]"); | |
| i++; | |
| continue; | |
| if (SENSITIVE_ARGV_FLAGS.has(arg) && i + 1 < argv.length) { | |
| out.push(arg, "[REDACTED]"); | |
| i++; | |
| continue; | |
| } | |
| // Note: if a sensitive flag is the last element with no following value, | |
| // there is nothing to redact — pass it through unchanged. |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/config/io.ts
Line: 128-131
Comment:
**Sensitive flag as the last argument is not redacted**
When a sensitive flag appears as the very last element of `argv` (no following value), the guard `i + 1 < argv.length` is `false`, so the code falls through to `out.push(arg)` — the flag name is written to the audit log without any indication that it may have carried a value in a different invocation. This is not a credential leak by itself (no value is present), but it means the logic silently does nothing for the malformed/truncated case rather than treating it uniformly.
More importantly, because `.slice(0, 8)` is applied to the redacted array **after** redaction, consider the following scenario:
```
process.argv = ["node", "openclaw", "a", "b", "c", "d", "e", "--token", "secretval", ...]
// idx: 0 1 2 3 4 5 6 7 8
```
`redactArgv()` runs on the **full** array. At index 7 (`--token`), `i + 1 = 8 < len` is true, so both `--token` and `[REDACTED]` are pushed and `i` advances to 8 — the result array still has `--token` at index 7 and `[REDACTED]` at index 8. After `.slice(0, 8)` the entry at index 8 is cut off, leaving `--token` without `[REDACTED]`. No value is exposed (the slice removed it either way), but the flag name is still present in the record.
This is safe against leaking values, but it may be worth adding a comment near the `slice(0, 8)` call sites to document that redaction must run before slicing (to avoid accidentally re-ordering them in a future refactor):
```suggestion
if (SENSITIVE_ARGV_FLAGS.has(arg) && i + 1 < argv.length) {
out.push(arg, "[REDACTED]");
i++;
continue;
}
// Note: if a sensitive flag is the last element with no following value,
// there is nothing to redact — pass it through unchanged.
```
How can I resolve this? If you propose a fix, please make it concise.| function redactArgv(argv: string[]): string[] { | ||
| const out: string[] = []; | ||
| for (let i = 0; i < argv.length; i++) { | ||
| const arg = argv[i] ?? ""; | ||
| // Handle --flag=value form | ||
| const eqIdx = arg.indexOf("="); | ||
| if (eqIdx !== -1) { | ||
| const flag = arg.slice(0, eqIdx); | ||
| if (SENSITIVE_ARGV_FLAGS.has(flag)) { | ||
| out.push(`${flag}=[REDACTED]`); | ||
| continue; | ||
| } | ||
| } | ||
| // Handle --flag <value> form | ||
| if (SENSITIVE_ARGV_FLAGS.has(arg) && i + 1 < argv.length) { | ||
| out.push(arg, "[REDACTED]"); | ||
| i++; | ||
| continue; | ||
| } | ||
| out.push(arg); | ||
| } | ||
| return out; | ||
| } |
There was a problem hiding this comment.
No unit tests for security-critical
redactArgv helper
redactArgv() is an untested security helper guarding against credential leaks in the audit log. The existing test files (io.write-config.test.ts, io.observe-config.test.ts) contain no coverage of the argv redaction behavior.
Given the security sensitivity and the two distinct code paths (--flag value and --flag=value), this function is a strong candidate for unit tests. Because it is currently module-private it cannot be tested in isolation — it either needs to be exported for a colocated test or covered via the full audit-log write path.
Suggested cases worth covering:
--flag valueform is redacted--flag=valueform is redacted- Non-sensitive flags pass through unchanged
- Sensitive flag as the last element (no following value) does not crash
- Chained sensitive flags are all independently redacted
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/config/io.ts
Line: 114-136
Comment:
**No unit tests for security-critical `redactArgv` helper**
`redactArgv()` is an untested security helper guarding against credential leaks in the audit log. The existing test files (`io.write-config.test.ts`, `io.observe-config.test.ts`) contain no coverage of the argv redaction behavior.
Given the security sensitivity and the two distinct code paths (`--flag value` and `--flag=value`), this function is a strong candidate for unit tests. Because it is currently module-private it cannot be tested in isolation — it either needs to be exported for a colocated test or covered via the full audit-log write path.
Suggested cases worth covering:
- `--flag value` form is redacted
- `--flag=value` form is redacted
- Non-sensitive flags pass through unchanged
- Sensitive flag as the last element (no following value) does not crash
- Chained sensitive flags are all independently redacted
How can I resolve this? If you propose a fix, please make it concise.
Summary
The config tamper detection audit log (
config-audit.jsonl) was recording full CLI argument vectors including plaintext gateway tokens, bot tokens, and API keys — making a security control itself a credential leak vector.Fix
Added a
redactArgv()helper function andSENSITIVE_ARGV_FLAGSconstant insrc/config/io.tsthat scrubs known secret-bearing flag values before they are written to the audit log.Flags redacted:
--token,--bot-token,--app-token,--access-token,--gateway-token,--password,--api-key,--secret,--secret-key,--secret-inputBoth
--flag valueand--flag=valueforms are handled.Changes
src/config/io.ts— addedSENSITIVE_ARGV_FLAGSset andredactArgv()function; replaced all 3 audit-recordargv: process.argv.slice(0, 8)writes withargv: redactArgv(process.argv).slice(0, 8)Fixes #60826