Skip to content

Commit c440454

Browse files
committed
fix(mcp): harden stdio env filtering
1 parent 52154ed commit c440454

2 files changed

Lines changed: 49 additions & 4 deletions

File tree

src/agents/mcp-config-shared.ts

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,43 @@
44
* MCP transport setup uses these functions to normalize loose JSON config into
55
* string records/arrays while dropping unsafe host environment variables.
66
*/
7-
import { isDangerousHostEnvVarName } from "../infra/host-env-security.js";
7+
import {
8+
isDangerousHostEnvVarName,
9+
isDangerousHostInheritedEnvVarName,
10+
normalizeEnvVarKey,
11+
} from "../infra/host-env-security.js";
12+
13+
const MCP_EXPLICIT_CREDENTIAL_ENV_KEYS = new Set([
14+
// Explicit MCP server credentials are operator-configured auth inputs, not
15+
// inherited host config pivots. If policy adds credential keys, add only
16+
// direct credentials here; keep loader/search/config pivots blocked.
17+
"AMQP_URL",
18+
"AWS_ACCESS_KEY_ID",
19+
"AWS_SECRET_ACCESS_KEY",
20+
"AWS_SECURITY_TOKEN",
21+
"AWS_SESSION_TOKEN",
22+
"AZURE_CLIENT_ID",
23+
"AZURE_CLIENT_SECRET",
24+
"DATABASE_URL",
25+
"GH_TOKEN",
26+
"GITHUB_TOKEN",
27+
"GITLAB_TOKEN",
28+
"MONGODB_URI",
29+
"NODE_AUTH_TOKEN",
30+
"NPM_TOKEN",
31+
"REDIS_URL",
32+
]);
33+
34+
function isDangerousMcpStdioEnvVarName(rawKey: string): boolean {
35+
if (isDangerousHostEnvVarName(rawKey)) {
36+
return true;
37+
}
38+
const key = normalizeEnvVarKey(rawKey);
39+
if (!key || MCP_EXPLICIT_CREDENTIAL_ENV_KEYS.has(key.toUpperCase())) {
40+
return false;
41+
}
42+
return isDangerousHostInheritedEnvVarName(key);
43+
}
844

945
/** Returns whether a value is a plain MCP config record. */
1046
export function isMcpConfigRecord(value: unknown): value is Record<string, unknown> {
@@ -63,7 +99,7 @@ export function toMcpEnvRecord(
6399
return toMcpFilteredStringRecord(value, {
64100
...options,
65101
preserveEmptyWhenKeysDropped: true,
66-
shouldDropKey: (key) => isDangerousHostEnvVarName(key),
102+
shouldDropKey: (key) => isDangerousMcpStdioEnvVarName(key),
67103
});
68104
}
69105

src/agents/mcp-transport-config.test.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,9 @@ describe("resolveMcpTransportConfig", () => {
4949
});
5050

5151
it("drops dangerous env overrides from stdio config", () => {
52-
// Stdio env is executable process input. Block loader/shell hook variables
53-
// while preserving ordinary provider tokens and scalar env values.
52+
// Stdio env is inherited executable process input. Block loader/shell hook
53+
// variables and child-process config pivots while preserving explicit MCP
54+
// credentials and ordinary scalar env values.
5455
const resolved = resolveMcpTransportConfig("probe", {
5556
command: "node",
5657
env: {
@@ -62,6 +63,8 @@ describe("resolveMcpTransportConfig", () => {
6263
NODE_OPTIONS: "--require=./evil.js",
6364
LD_PRELOAD: "/tmp/pwn.so",
6465
BASH_ENV: "/tmp/pwn.sh",
66+
ANSIBLE_CONFIG: "/tmp/evil-ansible.cfg",
67+
TF_CLI_CONFIG_FILE: "/tmp/evil-terraform.rc",
6568
},
6669
});
6770

@@ -92,6 +95,12 @@ describe("resolveMcpTransportConfig", () => {
9295
expect(logWarn).toHaveBeenCalledWith(
9396
'bundle-mcp: server "probe": env "BASH_ENV" is blocked for stdio startup safety and was ignored.',
9497
);
98+
expect(logWarn).toHaveBeenCalledWith(
99+
'bundle-mcp: server "probe": env "ANSIBLE_CONFIG" is blocked for stdio startup safety and was ignored.',
100+
);
101+
expect(logWarn).toHaveBeenCalledWith(
102+
'bundle-mcp: server "probe": env "TF_CLI_CONFIG_FILE" is blocked for stdio startup safety and was ignored.',
103+
);
95104
});
96105

97106
it("uses an explicit empty stdio env when all configured env keys are blocked", () => {

0 commit comments

Comments
 (0)