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.py — DANGEROUS_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
Tracking write-up for a Pass B application-security finding. Fix branch pushed to this fork; cross-fork PR into
plmbr/notebook-intelligencecould 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 upstreamSECURITY.mdprivate-disclosure policy, the public PR body is limited to fix rationale; full attacker model is here.Component
notebook_intelligence/mcp_policy.py—DANGEROUS_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
mcp.json/claude mcp addconfig — 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 onmcp_stdio_command_allowlistto pin the launchable command to a trusted wrapper.envmapping of that MCP server entry, plus an attacker-controlled file placed on disk (identical prerequisite to the already-deniedLD_PRELOAD.so/PYTHONPATHmodule).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.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 nexticonv/UTF-8 conversion. It injects code into any glibc-linked allowed binary, exactly likeLD_PRELOAD, and is more general than the interpreter vars (does not require the wrapper to be an interpreter).PERL5OPT/PERL5LIB— Perl analogue ofPYTHONPATH:PERL5OPTcan smuggle-M<module>;PERL5LIBprepends attacker dirs to@INC.RUBYOPT/RUBYLIB— Ruby analogue:-rrequire 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_keysdenies 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 inbuilt_in_toolsets.pyand thescripts/review-pr.shprompt-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