Skip to content

[security] MCP stdio env denylist is missing GCONV_PATH / PERL5* / RUBY* loader-injection keys #49

Description

@herikwebb

Tracking write-up for a Pass B application-security finding. Fix branch pushed to this fork; cross-fork PR into plmbr/notebook-intelligence could not be opened automatically (this automation's GitHub App is installed on the fork only), so it must be opened manually — compare URL at the bottom. Per the upstream SECURITY.md private-disclosure policy, the public PR body is limited to fix rationale; full attacker model is here.

Component

notebook_intelligence/mcp_policy.pyDANGEROUS_MCP_ENV_KEYS / reject_dangerous_env_keys() (the MCP stdio command-allowlist backstop).

Severity

Low–Medium (defense-in-depth completeness gap). Only relevant in the non-default hardened configuration where an admin has set mcp_stdio_command_allowlist; in the default per-user deployment there is no privilege boundary and this is not exploitable.

Attacker model

  • Attacker: a party who can get a crafted MCP server entry into the user's mcp.json / claude mcp add config — e.g. a social-engineered "add this MCP server" instruction, or a prompt-injected LLM that proposes an MCP config — in a deployment that relies on mcp_stdio_command_allowlist to pin the launchable command to a trusted wrapper.
  • Controlled input: the env mapping of that MCP server entry, plus an attacker-controlled file placed on disk (identical prerequisite to the already-denied LD_PRELOAD .so / PYTHONPATH module).
  • Path to code: config → reject_dangerous_env_keys(env) (passes, because the key is not in the denylist) → server_env.update(env)StdioServerParameters → the SDK spawns the pinned wrapper with the poisoned env.
  • Impact: code execution in the spawned MCP subprocess, escaping the command allowlist containment that is the whole point of the control in a hardened multi-tenant deployment.

The gap

The denylist's stated job (per its own comment) is to reject env keys that "convert a name-only allowlist into a full RCE" so "the binary-name allowlist isn't a false sense of security." It lists PATH, LD_PRELOAD, LD_LIBRARY_PATH, LD_AUDIT, DYLD_*, PYTHONPATH/PYTHONSTARTUP/PYTHONHOME, NODE_OPTIONS/NODE_PATH, BASH_ENV, ENV — but omits members of the same class:

  • GCONV_PATH — glibc loads an attacker-supplied charset-conversion module (.so) from this directory on the next iconv/UTF-8 conversion. It injects code into any glibc-linked allowed binary, exactly like LD_PRELOAD, and is more general than the interpreter vars (does not require the wrapper to be an interpreter).
  • PERL5OPT / PERL5LIB — Perl analogue of PYTHONPATH: PERL5OPT can smuggle -M<module>; PERL5LIB prepends attacker dirs to @INC.
  • RUBYOPT / RUBYLIB — Ruby analogue: -r require injection / load-path prepend.

Fix

Additive — add the five keys to DANGEROUS_MCP_ENV_KEYS, plus a parametrized test alongside the existing classic-bypass cases. No change for any currently-accepted config. Logic verified directly (reject_dangerous_env_keys denies each of the new keys, including case/whitespace-normalized forms; safe keys and existing entries unaffected).

How this was found

Scheduled Pass B application-security review of upstream tip plmbr/notebook-intelligence@d9f76a5 (v5.2.1). Four independent surface reviews (HTTP/auth, built-in file/command tools, skills import/reconciler, MCP policy/process-exec) were run; this was the only validated, non-duplicate finding. All other surfaces were clean; the previously-tracked symlink issues in built_in_toolsets.py and the scripts/review-pr.sh prompt-injection are excluded as already tracked.

Manually open the upstream PR

https://github.com/plmbr/notebook-intelligence/compare/main...herikwebb:security/fix-mcp-env-denylist-loader-vars-20260708122516-31416?expand=1

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions