You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
[security] Remote MCP headers templates read arbitrary process.env — the stdio allowlist from #208 is not applied on the HTTP path
Summary
expandHeaderTemplate resolves ${VAR} in a plugin's declared MCP headers by reading process.env[VAR] with no allowlist. The result is attached to the outbound HTTP request as http_headers.
PR #208 ("fix(mcp): restrict stdio server environment") deliberately narrowed the environment handed to stdio subprocesses to DEFAULT_INHERITED_ENV_VARS. That boundary is not applied on the HTTP path, so the same third-party manifest can read any environment variable the StepCode process holds and send it to an arbitrary remote URL.
A url-type MCP server executes nothing on the user's machine, so it is reasonable for a user to assume it cannot read their environment.
Impact
A plugin that declares a url-type MCP server can exfiltrate API keys, CI tokens and any other environment variable of the StepCode process to a server the plugin author controls. The declared tool surface looks harmless.
Reproduction
Real run against main at 519e4de with the repository's own runner, using canary values so nothing real is read.
import{test,expect}from"vitest";import{expandHeaderTemplate}from"../src/step/mcp.ts";import{DEFAULT_INHERITED_ENV_VARS}from"@modelcontextprotocol/sdk/client/stdio.js";test("header templates expand any env var",()=>{constcanaries=["AWS_SECRET_ACCESS_KEY","AWS_SESSION_TOKEN","GITHUB_TOKEN","OPENAI_API_KEY","ANTHROPIC_API_KEY","DB_PASSWORD","SCA_TOTALLY_UNRELATED_VAR",];for(constnameofcanaries)process.env[name]=`CANARY-${name}`;for(constnameofcanaries){console.log(`\${${name}} -> ${JSON.stringify(expandHeaderTemplate(`\${${name}}`))}`);}console.log("stdio allowlist =",JSON.stringify(DEFAULT_INHERITED_ENV_VARS));expect(expandHeaderTemplate("${AWS_SECRET_ACCESS_KEY}")).toBe("CANARY-AWS_SECRET_ACCESS_KEY");expect(DEFAULT_INHERITED_ENV_VARS).not.toContain("AWS_SECRET_ACCESS_KEY");});
Every variable expands. None of the credential-shaped names are in the stdio allowlist, which is the asymmetry this report is about.
Root cause
packages/coding-agent/src/step/mcp.ts:424 — const value = name === undefined ? undefined : process.env[name]; inside expandHeaderTemplate, with no filtering of name.
packages/coding-agent/src/step/mcp.ts:476-486 — applyPluginHeaderAliases calls it for every string entry in a manifest's headers, then stores the expanded value on the declaration.
packages/coding-agent/src/step/mcp.ts:368-370 — discoverStepMcpServers applies this to every plugin mcpServers entry, so the path is reachable from any installed plugin.
Contrast: packages/coding-agent/src/step/mcp-environment.ts restricts the stdio child environment to DEFAULT_INHERITED_ENV_VARS (the change from fix(mcp): restrict stdio server environment #208).
Attribution
expandHeaderTemplate predates #208 — commit 0903f1f only moved the line. The asymmetry is that #208 tightened the stdio side without applying the same constraint to headers. This reads as an omission in #208 rather than an intentional design choice, which is why I am reporting it against the current boundary rather than asking for a behaviour change.
Suggested fix
Apply an explicit allowlist to expandHeaderTemplate rather than to the transport, so both paths share one rule. Two reasonable options:
restrict ${VAR} interpolation to DEFAULT_INHERITED_ENV_VARS, or
require the manifest to declare which variables it may reference and validate name against that declaration.
The first is the smaller change and matches what #208 already established for stdio.
Verification performed
Repository: stepfun-ai/Step-Code, main at 519e4de.
Reproduction run with the repo's pinned vitest (4.1.9) on Node 24.19.0, Windows.
Canary environment variables only; no real secret was read or transmitted, and no network request was made.
[security] Remote MCP
headerstemplates read arbitraryprocess.env— the stdio allowlist from #208 is not applied on the HTTP pathSummary
expandHeaderTemplateresolves${VAR}in a plugin's declared MCPheadersby readingprocess.env[VAR]with no allowlist. The result is attached to the outbound HTTP request ashttp_headers.PR #208 ("fix(mcp): restrict stdio server environment") deliberately narrowed the environment handed to stdio subprocesses to
DEFAULT_INHERITED_ENV_VARS. That boundary is not applied on the HTTP path, so the same third-party manifest can read any environment variable the StepCode process holds and send it to an arbitrary remote URL.A url-type MCP server executes nothing on the user's machine, so it is reasonable for a user to assume it cannot read their environment.
Impact
A plugin that declares a
url-type MCP server can exfiltrate API keys, CI tokens and any other environment variable of the StepCode process to a server the plugin author controls. The declared tool surface looks harmless.Reproduction
Real run against
mainat519e4dewith the repository's own runner, using canary values so nothing real is read.Observed output:
Every variable expands. None of the credential-shaped names are in the stdio allowlist, which is the asymmetry this report is about.
Root cause
packages/coding-agent/src/step/mcp.ts:424—const value = name === undefined ? undefined : process.env[name];insideexpandHeaderTemplate, with no filtering ofname.packages/coding-agent/src/step/mcp.ts:476-486—applyPluginHeaderAliasescalls it for every string entry in a manifest'sheaders, then stores the expanded value on the declaration.packages/coding-agent/src/step/mcp.ts:368-370—discoverStepMcpServersapplies this to every pluginmcpServersentry, so the path is reachable from any installed plugin.packages/coding-agent/src/step/mcp-environment.tsrestricts the stdio child environment toDEFAULT_INHERITED_ENV_VARS(the change from fix(mcp): restrict stdio server environment #208).Attribution
expandHeaderTemplatepredates #208 — commit0903f1fonly moved the line. The asymmetry is that #208 tightened the stdio side without applying the same constraint to headers. This reads as an omission in #208 rather than an intentional design choice, which is why I am reporting it against the current boundary rather than asking for a behaviour change.Suggested fix
Apply an explicit allowlist to
expandHeaderTemplaterather than to the transport, so both paths share one rule. Two reasonable options:${VAR}interpolation toDEFAULT_INHERITED_ENV_VARS, ornameagainst that declaration.The first is the smaller change and matches what #208 already established for stdio.
Verification performed
stepfun-ai/Step-Code,mainat519e4de.vitest(4.1.9) on Node 24.19.0, Windows.