fix(secrets): scope message SecretRef resolution and harden doctor/status paths#48728
Conversation
🔒 Aisle Security AnalysisWe found 3 potential security issue(s) in this PR:
1. 🟠
|
| Property | Value |
|---|---|
| Severity | High |
| CWE | CWE-200 |
| Location | src/cli/command-secret-gateway.ts:540-556 |
Description
resolveCommandSecretRefsViaGateway introduces an allowedPaths parameter intended to scope which SecretRef paths are enforced/resolved (e.g., per-channel/per-account scoping). However, in the gateway-backed path, all assignments returned by the gateway are applied to resolvedConfig without filtering by allowedPaths.
This means a caller can request broad targetIds (e.g., channels.discord.accounts.*.token) while supplying a narrow allowedPaths (e.g., one account token), but the client will still:
- ask the gateway to resolve all secrets matching the broad
targetIds - merge all returned assignments into
resolvedConfig, including secrets for disallowed paths - then run unresolved/enforcement analysis only over
allowedPaths, so the extra secrets are not reflected intargetStatesByPath
Impact:
- Secret isolation between accounts/channels is broken: secrets outside the intended scope may be resolved and become available in-process to downstream code.
- If any later code path exposes parts of the resolved config (debug/status output, tool responses, plugin errors, etc.), this becomes an information disclosure risk.
- A compromised/malicious gateway could also inject assignments to any existing secret path in config (since the client does not verify assignments correspond to requested/allowed targets).
Vulnerable code:
for (const assignment of parsed.assignments) {
const pathSegments = assignment.pathSegments.filter((segment) => segment.length > 0);
if (pathSegments.length === 0) {
continue;
}
setPathExistingStrict(resolvedConfig, pathSegments, assignment.value);
}No check is performed against params.allowedPaths before mutating resolvedConfig.
Recommendation
Filter (and ideally validate) gateway assignments before applying them.
Minimum fix (enforce allowedPaths when present):
for (const assignment of parsed.assignments) {
const pathSegments = assignment.pathSegments.filter((s) => s.length > 0);
if (pathSegments.length === 0) continue;
const path = pathSegments.join(".");
if (params.allowedPaths && !params.allowedPaths.has(path)) {
continue; // ignore out-of-scope assignments
}
setPathExistingStrict(resolvedConfig, pathSegments, assignment.value);
}Stronger fix (also prevent unexpected assignments even when allowedPaths is undefined):
- Build an
allowedAssignmentPathsset fromdiscoverConfigSecretTargetsByIds(config, targetIds)(optionally intersect withallowedPaths) and only apply assignments whosepathis in that set. - Consider extending the gateway protocol to accept
allowedPathsso the gateway itself does not resolve/return out-of-scope secrets.
2. 🟡 Sensitive data exposure via unredacted plugin error stack/message logging (message-actions)
| Property | Value |
|---|---|
| Severity | Medium |
| CWE | CWE-532 |
| Location | src/channels/plugins/message-actions.ts:20-37 |
Description
listChannelMessageActions() / listChannelMessageCapabilities() now catch exceptions thrown by channel plugins and log raw Error.stack (or Error.message) via defaultRuntime.error.
This is risky because:
defaultRuntime.errorwrites directly toconsole.error(stderr) and may be captured by log aggregation/telemetry.- Stack traces and error messages commonly include sensitive values such as:
- request URLs containing embedded credentials (e.g., Telegram Bot API URLs embed the bot token as
/bot<token>/...), Authorization: Bearer ...headers,- API keys/tokens included by third-party plugin error messages.
- request URLs containing embedded credentials (e.g., Telegram Bot API URLs embed the bot token as
- This code does not apply any existing redaction utilities (e.g.
redactSensitiveText()insrc/logging/redact.ts, already used insrc/infra/errors.ts).
Vulnerable code:
const stack = params.error instanceof Error && params.error.stack ? params.error.stack : null;
defaultRuntime.error?.(
`[message-actions] ${params.pluginId}.actions.${params.operation} failed: ${stack ?? message}`,
);Impact: secrets present in plugin exception messages/stacks can be written to logs.
Recommendation
Redact sensitive data before logging and avoid logging full stacks by default.
- Prefer logging a short, redacted message.
- If you must log stacks, redact them first.
Example using existing redaction helper:
import { redactSensitiveText } from "../../logging/redact.js";
const message = params.error instanceof Error ? params.error.message : String(params.error);
const stackOrMessage =
params.error instanceof Error && params.error.stack ? params.error.stack : message;
defaultRuntime.error?.(
`[message-actions] ${params.pluginId}.actions.${params.operation} failed: ` +
redactSensitiveText(stackOrMessage),
);Optionally:
- gate stack logging behind a verbose/debug flag
- log an error code/classification rather than the raw stack
3. 🔵 Sensitive data exposure via unredacted plugin config error logging (channel-selection)
| Property | Value |
|---|---|
| Severity | Low |
| CWE | CWE-532 |
| Location | src/infra/outbound/channel-selection.ts:63-80 |
Description
listConfiguredMessageChannels() now catches plugin resolveAccount() / isConfigured() exceptions and logs the raw Error.message via defaultRuntime.error.
This is risky because:
- These errors originate in plugin code and may include sensitive values (tokens, secrets, credential-bearing URLs, or headers) in their message text.
- The project already has a redaction mechanism (
redactSensitiveText()insrc/logging/redact.ts, used e.g. bysrc/infra/errors.ts), but it is not applied here. defaultRuntime.errorwrites directly to stderr (console.error), which may be collected/forwarded.
Vulnerable code:
const message = params.error instanceof Error ? params.error.message : String(params.error);
...
defaultRuntime.error?.(
`[channel-selection] ${params.pluginId}(${params.accountId}) ${params.operation} failed: ${message}`,
);Impact: secrets embedded in plugin exception messages can be written to logs.
Recommendation
Redact sensitive strings before logging, using the existing project helper.
import { redactSensitiveText } from "../../logging/redact.js";
const message = params.error instanceof Error ? params.error.message : String(params.error);
defaultRuntime.error?.(
`[channel-selection] ${params.pluginId}(${params.accountId}) ${params.operation} failed: ` +
redactSensitiveText(message),
);Additionally consider:
- logging a stable error code/classification rather than arbitrary plugin messages
- bounding the dedupe Set (LRU/TTL/max size) to avoid retaining large/unique messages
Analyzed PR: #48728 at commit 1501008
Last updated on: 2026-03-17T05:59:13Z
Greptile SummaryThis PR hardens the SecretRef resolution pipeline by scoping secret enforcement to the active message target (channel + account) and guarding several previously-fatal paths in doctor, status, channel selection, and message action discovery. The overall approach is well-structured and the new abstractions ( Key changes and findings:
Confidence Score: 3/5
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 82b9f9cb7b
ℹ️ 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".
|
Addressed both Greptile points in ce5fd6034a. What changed:
Validation run:
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bc50762dda
ℹ️ 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".
| if (accountRoot !== "accounts") { | ||
| return true; | ||
| } | ||
| return accountId === params.accountId; |
There was a problem hiding this comment.
Compare scoped account IDs case-insensitively
The account-path filter uses a strict equality check for accountId, but account selection elsewhere is case-insensitive (for example via resolveAccountEntry), so configs that define account keys with different casing (e.g. Ops) are treated as non-matching here. In that case allowedPaths drops the selected account’s SecretRef paths, resolveCommandSecretRefsViaGateway skips resolving them, and message/tool actions can later fail with unresolved credentials even though the configured account is valid.
Useful? React with 👍 / 👎.
18a7b07 to
1501008
Compare
…atus paths (openclaw#48728) * fix(secrets): scope message runtime resolution and harden doctor/status * docs: align message/doctor/status SecretRef behavior notes * test(cli): accept scoped targetIds wiring in secret-resolution coverage * fix(secrets): keep scoped allowedPaths isolation and tighten coverage gate * fix(secrets): avoid default-account coercion in scoped target selection * test(doctor): cover inactive telegram secretref inspect path * docs Signed-off-by: joshavant <830519+joshavant@users.noreply.github.com> * changelog Signed-off-by: joshavant <830519+joshavant@users.noreply.github.com> --------- Signed-off-by: joshavant <830519+joshavant@users.noreply.github.com>
|
I think I’m running into the same underlying SecretRef/runtime-resolution problem in a few other CLI paths, not just the ones this PR touches @joshavant I’m seeing similar failures in other commands where an unrelated unresolved channel SecretRef seems to stop the command entirely. The examples below are from 2026.3.13 but I get the same errors when running from git-main as well user@host:~$ openclaw agents
🦞 OpenClaw 2026.3.13 (61d171a) — Say "stop" and I'll stop—say "ship" and we'll both learn a lesson.
14:21:31 [plugins] memory-lancedb: plugin registered (db: /home/user/.openclaw/memory/lancedb, lazy init)
Error: channels.discord.accounts.coder.token: unresolved SecretRef "file:openclaw:/channels/discord/coder". Resolve this command against an active gateway runtime snapshot before reading it.user@host:~$ openclaw doctor
🦞 OpenClaw 2026.3.13 (61d171a) — Half butler, half debugger, full crustacean.
┌ OpenClaw doctor
14:22:31 [plugins] memory-lancedb: plugin registered (db: /home/user/.openclaw/memory/lancedb, lazy init)
│
◇ Update
│ This install is not a git checkout.
│ Run `openclaw update` to update via your package manager (npm/pnpm), then rerun doctor.
│
◇ Gateway auth
│ Gateway token is managed via SecretRef and is currently unavailable.
│ Doctor will not overwrite gateway.auth.token with a plaintext value.
│ Resolve/rotate the external secret source, then rerun doctor.
│
Error: channels.discord.accounts.default.token: unresolved SecretRef "file:openclaw:/channels/discord/default". Resolve this command against an active gateway runtime snapshot before reading it.user@host:~$ openclaw gateway restart
🦞 OpenClaw 2026.3.13 (61d171a) — I've survived more breaking changes than your last three relationships.
14:22:41 [plugins] memory-lancedb: plugin registered (db: /home/user/.openclaw/memory/lancedb, lazy init)
⚠️ Unable to verify gateway token drift: gateway.auth.token SecretRef is configured but unavailable in this command path.
Restarted systemd service: openclaw-gateway.serviceMy question is whether the same fix pattern from this PR should be applied more broadly to other CLI commands too. More specifically:
If that read is right, I’d expect there are probably a few more commands that still need the same treatment. |
…atus paths (openclaw#48728) * fix(secrets): scope message runtime resolution and harden doctor/status * docs: align message/doctor/status SecretRef behavior notes * test(cli): accept scoped targetIds wiring in secret-resolution coverage * fix(secrets): keep scoped allowedPaths isolation and tighten coverage gate * fix(secrets): avoid default-account coercion in scoped target selection * test(doctor): cover inactive telegram secretref inspect path * docs Signed-off-by: joshavant <830519+joshavant@users.noreply.github.com> * changelog Signed-off-by: joshavant <830519+joshavant@users.noreply.github.com> --------- Signed-off-by: joshavant <830519+joshavant@users.noreply.github.com> (cherry picked from commit da34f81)
…atus paths (openclaw#48728) * fix(secrets): scope message runtime resolution and harden doctor/status * docs: align message/doctor/status SecretRef behavior notes * test(cli): accept scoped targetIds wiring in secret-resolution coverage * fix(secrets): keep scoped allowedPaths isolation and tighten coverage gate * fix(secrets): avoid default-account coercion in scoped target selection * test(doctor): cover inactive telegram secretref inspect path * docs Signed-off-by: joshavant <830519+joshavant@users.noreply.github.com> * changelog Signed-off-by: joshavant <830519+joshavant@users.noreply.github.com> --------- Signed-off-by: joshavant <830519+joshavant@users.noreply.github.com> (cherry picked from commit da34f81)
…atus paths (openclaw#48728) * fix(secrets): scope message runtime resolution and harden doctor/status * docs: align message/doctor/status SecretRef behavior notes * test(cli): accept scoped targetIds wiring in secret-resolution coverage * fix(secrets): keep scoped allowedPaths isolation and tighten coverage gate * fix(secrets): avoid default-account coercion in scoped target selection * test(doctor): cover inactive telegram secretref inspect path * docs Signed-off-by: joshavant <830519+joshavant@users.noreply.github.com> * changelog Signed-off-by: joshavant <830519+joshavant@users.noreply.github.com> --------- Signed-off-by: joshavant <830519+joshavant@users.noreply.github.com> (cherry picked from commit da34f81)
…atus paths (openclaw#48728) * fix(secrets): scope message runtime resolution and harden doctor/status * docs: align message/doctor/status SecretRef behavior notes * test(cli): accept scoped targetIds wiring in secret-resolution coverage * fix(secrets): keep scoped allowedPaths isolation and tighten coverage gate * fix(secrets): avoid default-account coercion in scoped target selection * test(doctor): cover inactive telegram secretref inspect path * docs Signed-off-by: joshavant <830519+joshavant@users.noreply.github.com> * changelog Signed-off-by: joshavant <830519+joshavant@users.noreply.github.com> --------- Signed-off-by: joshavant <830519+joshavant@users.noreply.github.com>
…atus paths (openclaw#48728) * fix(secrets): scope message runtime resolution and harden doctor/status * docs: align message/doctor/status SecretRef behavior notes * test(cli): accept scoped targetIds wiring in secret-resolution coverage * fix(secrets): keep scoped allowedPaths isolation and tighten coverage gate * fix(secrets): avoid default-account coercion in scoped target selection * test(doctor): cover inactive telegram secretref inspect path * docs Signed-off-by: joshavant <830519+joshavant@users.noreply.github.com> * changelog Signed-off-by: joshavant <830519+joshavant@users.noreply.github.com> --------- Signed-off-by: joshavant <830519+joshavant@users.noreply.github.com>
…atus paths (openclaw#48728) * fix(secrets): scope message runtime resolution and harden doctor/status * docs: align message/doctor/status SecretRef behavior notes * test(cli): accept scoped targetIds wiring in secret-resolution coverage * fix(secrets): keep scoped allowedPaths isolation and tighten coverage gate * fix(secrets): avoid default-account coercion in scoped target selection * test(doctor): cover inactive telegram secretref inspect path * docs Signed-off-by: joshavant <830519+joshavant@users.noreply.github.com> * changelog Signed-off-by: joshavant <830519+joshavant@users.noreply.github.com> --------- Signed-off-by: joshavant <830519+joshavant@users.noreply.github.com>
…atus paths (openclaw#48728) * fix(secrets): scope message runtime resolution and harden doctor/status * docs: align message/doctor/status SecretRef behavior notes * test(cli): accept scoped targetIds wiring in secret-resolution coverage * fix(secrets): keep scoped allowedPaths isolation and tighten coverage gate * fix(secrets): avoid default-account coercion in scoped target selection * test(doctor): cover inactive telegram secretref inspect path * docs Signed-off-by: joshavant <830519+joshavant@users.noreply.github.com> * changelog Signed-off-by: joshavant <830519+joshavant@users.noreply.github.com> --------- Signed-off-by: joshavant <830519+joshavant@users.noreply.github.com>
…atus paths (openclaw#48728) * fix(secrets): scope message runtime resolution and harden doctor/status * docs: align message/doctor/status SecretRef behavior notes * test(cli): accept scoped targetIds wiring in secret-resolution coverage * fix(secrets): keep scoped allowedPaths isolation and tighten coverage gate * fix(secrets): avoid default-account coercion in scoped target selection * test(doctor): cover inactive telegram secretref inspect path * docs Signed-off-by: joshavant <830519+joshavant@users.noreply.github.com> * changelog Signed-off-by: joshavant <830519+joshavant@users.noreply.github.com> --------- Signed-off-by: joshavant <830519+joshavant@users.noreply.github.com>
Summary
This PR consolidates the SecretRef runtime/isolation cluster into one branch:
status --alltargetIdsProblem Clusters Addressed
Main Changes
Scoped runtime secret resolution for message flows
src/cli/message-secret-scope.tstargetIds+ optionalallowedPaths):src/cli/command-secret-targets.tsallowedPathsfiltering:src/cli/command-secret-gateway.tssrc/commands/message.tssrc/agents/tools/message-tool.tsRead/probe robustness (unrelated failures no longer fatal)
src/infra/outbound/channel-selection.tssrc/channels/plugins/message-actions.tsDiscord message action cfg threading
src/agents/tools/discord-actions-messaging.tsDoctor/status improvements
src/commands/doctor-config-flow.tsstatus --allnow surfaces SecretRef diagnostics in Overview + Diagnosis:src/commands/status-all.tssrc/commands/status-all/diagnosis.tsDocs updates
docs/cli/message.mddocs/cli/status.mddocs/cli/doctor.mddocs/channels/discord.mdTests
Added/updated tests for all touched surfaces:
src/cli/message-secret-scope.test.tssrc/cli/command-secret-targets.test.tssrc/cli/command-secret-gateway.test.tssrc/commands/message.test.tssrc/agents/tools/message-tool.test.tssrc/agents/tools/discord-actions.test.tssrc/infra/outbound/channel-selection.test.tssrc/channels/plugins/message-actions.test.tssrc/commands/doctor-config-flow*.test.tssrc/commands/status-all/report-lines.test.tssrc/cli/command-secret-resolution.coverage.test.tsValidation
Full suite
pnpm testmainbaseline in many unrelated suites after rebasing (for example global plugin-sdk/setup import issues such asformatCliCommand/formatDocsLinknot function, plus unrelated plugin/acp expectations).src/cli/command-secret-resolution.coverage.test.tsFocused branch suites
Manual smoke checks (isolated temp config)
Verified behavior with one unrelated unresolved channel SecretRef present:
openclaw message send --channel discord ... --dry-runopenclaw doctor --fix --non-interactive --yesopenclaw status --allRelated issues and PRs
Doctor/status unresolved SecretRef cluster
Message tool/read-probe eager resolution cluster