Skip to content

fix(mcp): block dangerous stdio env overrides#69540

Merged
steipete merged 2 commits intoopenclaw:mainfrom
drobison00:fix-mcp-stdio-env-filter
Apr 21, 2026
Merged

fix(mcp): block dangerous stdio env overrides#69540
steipete merged 2 commits intoopenclaw:mainfrom
drobison00:fix-mcp-stdio-env-filter

Conversation

@drobison00
Copy link
Copy Markdown
Contributor

Summary

Describe the problem and fix in 2–5 bullets:

If this PR fixes a plugin beta-release blocker, title it fix(<plugin-id>): beta blocker - <summary> and link the matching Beta blocker: <plugin-name> - <summary> issue labeled beta-blocker. Contributors cannot label PRs, so the title is the PR-side signal for maintainers and automation.

  • Problem: MCP stdio server configs accepted dangerous startup environment keys from config and passed them through to spawned subprocesses.
  • Why it matters: interpreter-startup keys such as NODE_OPTIONS, LD_PRELOAD, and BASH_ENV can execute code before the intended MCP server logic starts.
  • What changed: stdio env parsing now uses an env-specific filter backed by the existing host-env dangerous-key denylist, and targeted tests cover blocked env keys plus unchanged HTTP header parsing.
  • What did NOT change (scope boundary): HTTP header parsing, non-stdio transport behavior, and broader MCP config policy were left unchanged.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

  • Closes #
  • Related #
  • This PR fixes a bug or regression

Root Cause (if applicable)

For bug fixes or regressions, explain why this happened, not just what changed. Otherwise write N/A. If the cause is unclear, write Unknown.

  • Root cause: the stdio launch path reused a generic MCP string-record helper that coerced env entries to strings but never applied the existing dangerous host-env denylist.
  • Missing detection / guardrail: there was no env-specific helper or focused regression test asserting that dangerous startup env keys are stripped before subprocess launch.
  • Contributing context (if known): the shared helper is also used for HTTP headers, so the missing stdio-only policy stayed hidden behind a more permissive utility.

Regression Test Plan (if applicable)

For bug fixes or regressions, name the smallest reliable test coverage that should catch this. Otherwise write N/A.

  • Coverage level that should have caught this:
    • Unit test
    • Seam / integration test
    • End-to-end test
    • Existing coverage already sufficient
  • Target test or file: src/agents/mcp-transport-config.test.ts
  • Scenario the test should lock in: dangerous stdio env keys are dropped, safe values are still stringified, and env-like HTTP header names remain unchanged.
  • Why this is the smallest reliable guardrail: the bug is introduced during transport-config normalization before any subprocess is spawned, so a focused unit test catches the exact decision point.
  • Existing test that already covers this (if any): None.
  • If no new test is added, why not: N/A

User-visible / Behavior Changes

  • MCP stdio server configs now ignore dangerous startup env keys such as NODE_OPTIONS, LD_PRELOAD, and BASH_ENV.

Diagram (if applicable)

Before:
[stdio MCP config env] -> [generic string coercion] -> [spawned subprocess gets dangerous startup env]

After:
[stdio MCP config env] -> [env-specific denylist filter] -> [safe env only] -> [spawned subprocess]

Security Impact (required)

  • New permissions/capabilities? (Yes/No) No
  • Secrets/tokens handling changed? (Yes/No) No
  • New/changed network calls? (Yes/No) No
  • Command/tool execution surface changed? (Yes/No) Yes
  • Data access scope changed? (Yes/No) No
  • If any Yes, explain risk + mitigation: MCP subprocess launches no longer honor dangerous interpreter-startup env keys from config. The change narrows subprocess startup behavior by reusing the existing host-env denylist while preserving safe env entries and leaving HTTP header parsing unchanged.

Repro + Verification

Environment

  • OS: Linux 6.8.0-110-generic x86_64
  • Runtime/container: Codex task workspace
  • Model/provider:
  • Integration/channel (if any): MCP stdio
  • Relevant config (redacted): stdio MCP config with safe env entries plus dangerous startup env keys

Steps

  1. Construct an MCP stdio transport config with safe env values and dangerous startup keys such as NODE_OPTIONS, LD_PRELOAD, and BASH_ENV.
  2. Resolve the transport config through resolveMcpTransportConfig().
  3. Verify the resolved stdio env omits the dangerous keys while a separate HTTP transport config still preserves an env-like header name.

Expected

  • Dangerous startup env keys are omitted from stdio launch config, safe env values remain, and HTTP header parsing is unchanged.

Actual

  • Targeted unit tests pass with dangerous stdio env keys removed, safe env values preserved, and HTTP header parsing unchanged.

Evidence

Attach at least one:

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

Validation run:

  • node scripts/run-vitest.mjs run --config test/vitest/vitest.unit.config.ts src/agents/mcp-transport-config.test.ts (pass)

Human Verification (required)

What you personally verified (not just CI), and how:

  • Verified scenarios: reviewed the final diff and ran the targeted MCP transport unit test covering blocked dangerous stdio env keys, safe env stringification, and unchanged HTTP header parsing.
  • Edge cases checked: numeric and boolean safe env values still stringify correctly, and header keys that resemble env names still pass through the HTTP path.
  • What you did not verify: end-to-end subprocess spawn behavior with a live MCP server process or workspace-local config loading.

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

If a bot review conversation is addressed by this PR, resolve that conversation yourself. Do not leave bot review conversation cleanup for maintainers.

Compatibility / Migration

  • Backward compatible? (Yes/No) No
  • Config/env changes? (Yes/No) No
  • Migration needed? (Yes/No) Yes
  • If yes, exact upgrade steps: remove dangerous interpreter-startup env overrides from MCP stdio configs and replace them with safe env values or server-side configuration.

Risks and Mitigations

List only real risks for this PR. Add/remove entries as needed. If none, write None.

  • Risk: existing MCP stdio configs that relied on blocked startup env keys will stop receiving those overrides.
    • Mitigation: the denylist reuses the existing host-env policy, the change is limited to stdio env parsing, and the regression test locks safe env plus HTTP header behavior in place.

@openclaw-barnacle openclaw-barnacle Bot added agents Agent runtime and tooling size: S maintainer Maintainer-authored PR labels Apr 21, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 21, 2026

Greptile Summary

This PR closes a security gap in MCP stdio transport config parsing: the env field from user-provided stdio server configs was previously passed through a generic string-coercion helper (toMcpStringRecord) that applied no env-specific filtering, allowing interpreter-startup keys like NODE_OPTIONS, LD_PRELOAD, and BASH_ENV to reach spawned subprocesses. The fix introduces toMcpEnvRecord, which wraps the existing isDangerousHostEnvVarName / isDangerousHostEnvOverrideVarName denylist and uses it as a shouldDropKey predicate before string coercion. The HTTP header path is correctly left unchanged, and targeted unit tests cover both the blocked and the safe-passthrough paths.

Confidence Score: 5/5

Safe to merge — the security fix is correct and well-scoped; remaining findings are P2 quality suggestions.

The denylist reuse is sound: isDangerousHostEnvVarName covers blockedEverywhereKeys (NODE_OPTIONS, BASH_ENV, …) and blockedPrefixes (LD_, DYLD_, BASH_FUNC_), while isDangerousHostEnvOverrideVarName adds blockedOverrideOnlyKeys (credentials, HOME, …) and blockedOverridePrefixes. HTTP header parsing is untouched. The two remaining comments are P2 (no logging on drop; all-dangerous env collapsing to undefined) — both are pre-existing design characteristics that do not introduce new regressions.

No files require special attention for merge safety.

Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/agents/mcp-stdio.ts
Line: 38

Comment:
**Silent drop of dangerous keys — no diagnostic emitted**

`toMcpEnvRecord` is called without an `onDroppedEntry` callback, so when `NODE_OPTIONS`, `LD_PRELOAD`, `BASH_ENV`, or any other blocked key appears in the config, it is silently removed. The subprocess launches with the key absent and no warning is ever surfaced to the caller or logged. The `onDroppedEntry` hook was designed for exactly this case — passing it through to `resolveStdioMcpServerLaunchConfig`'s return value or emitting a structured log would make the behavior observable when users debug unexpected MCP server startup failures.

```suggestion
      env: toMcpEnvRecord(raw.env, {
        onDroppedEntry: (key) => {
          // TODO: surface via structured log when logger is available
          // e.g. logger?.warn(`MCP stdio env: dropping blocked key "${key}"`);
        },
      }),
```

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/agents/mcp-config-shared.ts
Line: 50-54

Comment:
**All-dangerous env config silently collapses to `undefined`**

If every key in the configured `env` object is on the denylist, `toMcpFilteredStringRecord` returns `undefined` (line 36: `entries.length > 0 ? ... : undefined`). The caller (`resolveStdioMcpServerLaunchConfig`) then sets `env: undefined` on the launch config, which causes the spawned subprocess to inherit the full parent-process environment rather than receiving an empty env. This means a maximally-restricted config (all-dangerous keys) ends up less restricted than intended because the subprocess inherits everything. If the intent is to pass no env at all, the function should return an empty object instead of `undefined` when all entries were filtered out by `shouldDropKey`.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix(mcp): block dangerous stdio env over..." | Re-trigger Greptile

Comment thread src/agents/mcp-stdio.ts Outdated
Comment thread src/agents/mcp-config-shared.ts
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c23f60dd4f

ℹ️ 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".

Comment thread src/agents/mcp-config-shared.ts Outdated
@steipete steipete force-pushed the fix-mcp-stdio-env-filter branch from c23f60d to 959ebaa Compare April 21, 2026 04:01
@aisle-research-bot
Copy link
Copy Markdown

aisle-research-bot Bot commented Apr 21, 2026

🔒 Aisle Security Analysis

We found 2 potential security issue(s) in this PR:

# Severity Title
1 🟡 Medium Empty environment {} can be passed to StdioClientTransport, causing spawned MCP servers to run with no inherited env (DoS/unintended behavior)
2 🔵 Low Log forging via unsanitized serverName and env var key in warning log
1. 🟡 Empty environment `{}` can be passed to StdioClientTransport, causing spawned MCP servers to run with no inherited env (DoS/unintended behavior)
Property Value
Severity Medium
CWE CWE-703
Location src/agents/mcp-config-shared.ts:7-58

Description

toMcpEnvRecord() intentionally drops dangerous env var overrides (e.g., NODE_OPTIONS, LD_PRELOAD). However, when all provided env keys are dropped, it now returns an explicit empty object {} rather than undefined.

That value is propagated into MCP stdio transport creation:

  • input: rawServer.env (config) in resolveStdioMcpServerLaunchConfig
  • filtering: toMcpEnvRecord() drops keys via isDangerousHostEnvVarName
  • sink: new StdioClientTransport({ ..., env: resolved.env }) (SDK likely spawns a child process with env)

In Node.js process spawning semantics, env: {} typically means do not inherit any environment from the parent, removing PATH, HOME, proxy/cert variables, etc. This can:

  • Prevent the MCP server from starting (e.g., command: "node" won’t resolve without PATH) → denial of service
  • Change runtime behavior in unexpected ways (missing HOME, TMPDIR, SSL_CERT_FILE, proxies), potentially causing the process to use fallback paths/configs

This behavior is reachable specifically when a config supplies only blocked env keys (as tested in mcp-transport-config.test.ts).

Recommendation

Avoid passing an explicit empty environment to the spawned stdio server.

Option A (preferred): inherit parent env, applying only safe overrides

  • Build env as { ...process.env, ...filteredEnv }, where filteredEnv excludes dangerous keys.
  • If filteredEnv is empty/undefined, omit env entirely so spawn inherits by default.

Example (at the point of transport creation):

const safeEnv = resolved.env;
const env = safeEnv ? { ...process.env, ...safeEnv } : undefined;
const transport = new StdioClientTransport({
  command: resolved.command,
  args: resolved.args,
  env,
  cwd: resolved.cwd,
  stderr: "pipe",
});

Option B: change toMcpEnvRecord to return undefined when all keys are dropped (remove preserveEmptyWhenKeysDropped), since the dangerous overrides are ignored anyway.

Add a regression test ensuring that when all env keys are blocked, the resulting transport/spawn still inherits process.env (or at least retains PATH).

2. 🔵 Log forging via unsanitized `serverName` and env var key in warning log
Property Value
Severity Low
CWE CWE-117
Location src/agents/mcp-transport-config.ts:99-104

Description

The warning callback onDroppedEnv logs untrusted strings directly into a log line:

  • serverName comes from MCP server map keys (config-controlled) and is interpolated without escaping
  • dropped env key comes from user-provided env object keys and is also interpolated without escaping
  • if either contains control characters (e.g., \n, \r, ANSI escapes), an attacker who can influence config can forge additional log lines, spoof severity/prefixes, or manipulate terminal output
  • while env values are not logged (good), the key is still attacker-controlled text

Vulnerable code:

onDroppedEnv: (key) => {
  logWarn(
    `bundle-mcp: server "${serverName}": env "${key}" is blocked for stdio startup safety and was ignored.`,
  );
},

Recommendation

Sanitize/escape untrusted fields before including them in log messages, or switch to structured logging.

Minimal mitigation: escape control characters (CR/LF, tabs, ANSI) in serverName and key prior to interpolation.

Example:

function escapeForLog(s: string): string {
  return s.replace(/[\r\n\t]/g, (c) => ({"\r":"\\r","\n":"\\n","\t":"\\t"}[c]!))
          .replace(/\x1b\[[0-9;]*m/g, ""); // strip ANSI color codes
}

onDroppedEnv: (key) => {
  logWarn(
    `bundle-mcp: server "${escapeForLog(serverName)}": env "${escapeForLog(key)}" is blocked for stdio startup safety and was ignored.`,
  );
},

Preferred: use a structured logger and pass fields separately (e.g., { serverName, key }) so the logging backend handles encoding safely.


Analyzed PR: #69540 at commit 54fbe30

Last updated on: 2026-04-21T06:59:39Z

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 959ebaa2f4

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/agents/mcp-stdio.ts
@@ -35,7 +38,7 @@ export function resolveStdioMcpServerLaunchConfig(raw: unknown): StdioMcpServerL
config: {
command: raw.command,
args: toMcpStringArray(raw.args),
env: toMcpStringRecord(raw.env),
env: toMcpEnvRecord(raw.env, { onDroppedEntry: options?.onDroppedEnv }),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Scope dangerous env filtering to MCP servers only

This change applies toMcpEnvRecord inside resolveStdioMcpServerLaunchConfig, which is also used by the bundle LSP runtime (src/agents/pi-bundle-lsp-runtime.ts:321). As a result, .lsp.json server env keys like PYTHONPATH/LD_LIBRARY_PATH are now stripped by the MCP denylist, and in the LSP path they are dropped silently because no onDroppedEnv callback is passed. That creates an unintended cross-surface regression where previously working LSP servers can fail to start or locate dependencies even though this PR is scoped to MCP transport hardening.

Useful? React with 👍 / 👎.

@steipete steipete force-pushed the fix-mcp-stdio-env-filter branch from 959ebaa to c71ddb9 Compare April 21, 2026 04:22
@steipete steipete force-pushed the fix-mcp-stdio-env-filter branch from c71ddb9 to 760d753 Compare April 21, 2026 04:38
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 760d753d16

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/agents/mcp-stdio.ts
@@ -35,7 +38,7 @@ export function resolveStdioMcpServerLaunchConfig(raw: unknown): StdioMcpServerL
config: {
command: raw.command,
args: toMcpStringArray(raw.args),
env: toMcpStringRecord(raw.env),
env: toMcpEnvRecord(raw.env, { onDroppedEntry: options?.onDroppedEnv }),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Limit stdio env denylist to MCP transport resolution

resolveStdioMcpServerLaunchConfig now always routes raw.env through toMcpEnvRecord, so every caller gets host-env denylist filtering, not just MCP transport parsing. The bundle LSP runtime still uses this same resolver (src/agents/pi-bundle-lsp-runtime.ts:321) for .lsp.json servers, which means LSP env keys are now silently removed and can break servers that rely on startup paths (for example PYTHONPATH/LD_LIBRARY_PATH). Fresh evidence in this commit: toMcpEnvRecord switched to isDangerousHostEnvVarName, and the current host policy marks PYTHONPATH and LD_ as blocked, so this regression remains even after allowing credential/proxy vars.

Useful? React with 👍 / 👎.

@steipete steipete force-pushed the fix-mcp-stdio-env-filter branch from 760d753 to 0226421 Compare April 21, 2026 04:56
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 02264216ab

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/agents/mcp-stdio.ts
@@ -35,7 +38,7 @@ export function resolveStdioMcpServerLaunchConfig(raw: unknown): StdioMcpServerL
config: {
command: raw.command,
args: toMcpStringArray(raw.args),
env: toMcpStringRecord(raw.env),
env: toMcpEnvRecord(raw.env, { onDroppedEntry: options?.onDroppedEnv }),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Scope stdio env filtering to MCP transport path

resolveStdioMcpServerLaunchConfig is shared by both MCP transport resolution and bundle LSP startup, so applying toMcpEnvRecord here strips env keys for .lsp.json servers too. In the current host policy, PYTHONPATH is blocked explicitly and LD_LIBRARY_PATH is blocked via the LD_ prefix, so LSP servers that depend on those variables can fail to start or resolve runtime dependencies; this regression is also silent in the LSP path because pi-bundle-lsp-runtime calls this resolver without onDroppedEnv logging. Fresh evidence: this commit switched this shared resolver from toMcpStringRecord to toMcpEnvRecord at the call site below.

Useful? React with 👍 / 👎.

@steipete steipete force-pushed the fix-mcp-stdio-env-filter branch from 0226421 to f49bf72 Compare April 21, 2026 05:10
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f49bf72963

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/agents/mcp-stdio.ts
@@ -35,7 +38,7 @@ export function resolveStdioMcpServerLaunchConfig(raw: unknown): StdioMcpServerL
config: {
command: raw.command,
args: toMcpStringArray(raw.args),
env: toMcpStringRecord(raw.env),
env: toMcpEnvRecord(raw.env, { onDroppedEntry: options?.onDroppedEnv }),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Apply dangerous-env filtering only in MCP transport path

resolveStdioMcpServerLaunchConfig is shared by MCP transport parsing and bundle LSP startup, so switching this resolver to toMcpEnvRecord strips env keys for .lsp.json servers as well. Fresh evidence in this commit is the new call at src/agents/mcp-stdio.ts:41; that resolver is still used by src/agents/pi-bundle-lsp-runtime.ts:321 with no MCP-only guard. Since isDangerousHostEnvVarName blocks runtime-path keys such as PYTHONPATH and LD_*, LSP servers that depend on those variables can fail to start or resolve dependencies, which is a regression outside the intended MCP hardening scope.

Useful? React with 👍 / 👎.

@steipete steipete force-pushed the fix-mcp-stdio-env-filter branch from f49bf72 to 54fbe30 Compare April 21, 2026 06:56
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 54fbe30014

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +100 to +103
onDroppedEnv: (key) => {
logWarn(
`bundle-mcp: server "${serverName}": env "${key}" is blocked for stdio startup safety and was ignored.`,
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Distinguish blocked env keys from malformed env values

resolveMcpTransportConfig now logs every dropped stdio env entry as "blocked for stdio startup safety", but toMcpEnvRecord calls the same drop callback for both dangerous keys and unsupported value types (see toMcpFilteredStringRecord branches in src/agents/mcp-config-shared.ts). With configs like env: { SAFE_VAR: { nested: true } }, the value is dropped because its type is invalid, yet the warning claims a security block, which misdiagnoses user config errors and makes MCP setup debugging materially harder.

Useful? React with 👍 / 👎.

@steipete steipete merged commit 85d86eb into openclaw:main Apr 21, 2026
92 checks passed
@steipete
Copy link
Copy Markdown
Contributor

Thanks @drobison00, landed in 85d86eb.

Validation before merge:

  • pnpm test src/agents/mcp-transport-config.test.ts
  • pnpm check:changed
  • GitHub checks green, including GPT-5.4 / Opus 4.6 parity gate

I also reviewed the post-merge security notes. The empty-env concern is covered by the MCP SDK behavior: StdioClientTransport merges supplied env over getDefaultEnvironment(), so {} still preserves the SDK safe default env (PATH, HOME, etc.) and does not inherit dangerous startup vars. The log-forging note was valid, so I pushed follow-up 9f054ee to sanitize config-controlled warning fields and added regression coverage in src/agents/mcp-transport-config.test.ts.

@mjamiv
Copy link
Copy Markdown
Contributor

mjamiv commented Apr 21, 2026

Appreciate the hardening intent; flagging one legitimate use case that this denylist as written would break silently.

When an MCP stdio server runs as a child of a gateway deployed behind an HTTP CONNECT proxy with an MITM TLS re-signing CA, the child's Node runtime needs:

  1. NODE_EXTRA_CA_CERTS=<path to internal CA> — otherwise every outbound HTTPS from the MCP server fails TLS handshake.
  2. NODE_OPTIONS=--require <path to a local proxy-env / HTTPS dispatcher shim> — because StdioClientTransport does not inherit parent env, and the child needs the same proxy-aware HTTPS dispatcher the parent gateway uses. Some libraries (e.g. MSAL-node) install their agent at import time, so --require is the only way to inject it ahead of first HTTPS call.

Without both, stdio MCP servers silently fail TLS handshake on first outbound request; cached access tokens can mask the failure until refresh. The ops-side gap behind this (TLS handshake gap for MCP stdio under proxy) is tracked at NVIDIA/OpenShell#886.

The current denylist removes NODE_OPTIONS entirely, which means the --require pattern above breaks silently in any deployment that needs it. Two possible refinements, either of which preserves the hardening goal:

  1. Narrow the denylist to dangerous sub-flags rather than the whole variable — parse the NODE_OPTIONS string and reject on a flag denylist (--inspect, --inspect-brk, --experimental-loader, --preserve-symlinks-main, etc.) while allowing --require <path>. Tighter security surface than a blanket block and keeps legitimate use cases working.
  2. Opt-in escape hatch at the config level — e.g. server.env.allowDangerous: ["NODE_OPTIONS"] that operators flip per-server after reviewing the risk. Keeps the safe default but provides a documented override.

Happy to send a patch for either approach.

One additional nit: the failure mode post-denylist is silent — the child just fails with network_error later. Please log a warn-level line when an env key is stripped, with the key name and server name, so operators can diagnose from startup logs without re-deriving the stdio env block by hand.

zhongmairen pushed a commit to agencyos-cn/openclaw-bad that referenced this pull request Apr 21, 2026
* 'main' of https://github.com/openclaw/openclaw: (653 commits)
  docs(changelog): deduplicate openclaw#67800 entries in Unreleased (openclaw#69670)
  fix(agents): honor explicit long Anthropic cache TTL on custom hosts (openclaw#67800)
  fix: fix Telegram media file delivery (openclaw#69641)
  fix(media): preserve outbound attachment filenames
  fix(media): parse lowercase media directives
  fix(bluebubbles): add opt-in coalesceSameSenderDms for split-send DMs (openclaw#69258)
  fix: centralize provider thinking profiles
  docs: prepare 2026.4.20 changelog
  fix: stage ACP and Codex runtime deps
  fix(gateway): drop stale service env on reinstall
  test: add bundled channel dependency Docker smoke
  test: relax detached task recovery timing assertion
  fix: ignore placeholder shells in runtime detection (openclaw#69308)
  shell: fall back to sh when SHELL is /usr/bin/false or nologin
  fix(browser): clarify DevToolsActivePort attach failures
  fix: sanitize mcp transport warning fields
  fix: launch Windows startup gateway directly
  fix(openai-codex): normalize legacy copilot transport
  fix: narrow MCP stdio env safety filter (openclaw#69540)
  fix(mcp): block dangerous stdio env overrides
  ...
gdibble pushed a commit to gdibble/openclaw that referenced this pull request Apr 21, 2026
ogt-redknie pushed a commit to ogt-redknie/OPENX that referenced this pull request May 2, 2026
zhonghe0615 pushed a commit to zhonghe0615/openclaw that referenced this pull request May 7, 2026
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling maintainer Maintainer-authored PR size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants