Skip to content

fix(mcp): replace process.env leak with getDefaultEnvironment() in MCP child processes#306

Open
Correctover wants to merge 4 commits into
OpenCoworkAI:mainfrom
Correctover:fix/mcp-env-security
Open

fix(mcp): replace process.env leak with getDefaultEnvironment() in MCP child processes#306
Correctover wants to merge 4 commits into
OpenCoworkAI:mainfrom
Correctover:fix/mcp-env-security

Conversation

@Correctover

Copy link
Copy Markdown

Summary

Fixes #305resolveBaseEnv() starts with { ...process.env } and merges the full login shell environment, then passes everything to StdioClientTransport. Any MCP server spawned by OpenCowork inherits the entire Electron app environment, including API keys and cloud credentials.

Changes

Before After
{ ...process.env } (all vars) { ...getDefaultEnvironment() } (6 safe vars)
Logs OPENAI_API_KEY / ANTHROPIC_API_KEY presence Logs only envKeysCount + hasCustomEnv

What getDefaultEnvironment() provides (from MCP SDK):

  • Unix: HOME, LOGNAME, PATH, SHELL, TERM, USER
  • Windows: HOMEDRIVE, HOMEPATH, LOCALAPPDATA, PATH, PROCESSOR_ARCHITECTURE, SYSTEMDRIVE, SYSTEMROOT, TEMP, USERNAME, USERPROFILE

What is excluded:

  • OPENAI_API_KEY, ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN
  • AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY
  • GITHUB_TOKEN, DATABASE_URL, SSH keys
  • Any other sensitive env var

What is preserved:

  • PATH merging logic for packaged Electron apps (critical for npx resolution)
  • User-configured env vars via config.env (the only intentional way to pass secrets)
  • Shell environment merge for macOS/Linux (still works, but only enriches safe base)

Attack scenario (before fix)

  1. User adds a malicious MCP server in OpenCowork config
  2. OpenCowork spawns it via StdioClientTransport with full process.env
  3. Malicious server reads process.env → gets all API keys, cloud creds, auth tokens
  4. Attacker exfiltrates credentials to their server

Attack scenario (after fix)

  1. User adds a malicious MCP server
  2. OpenCowork spawns it with only 6 safe env vars + user-configured env
  3. No API keys, no cloud creds, no auth tokens in the child process
  4. Malicious server can only see what the user explicitly configured

Testing

  • getDefaultEnvironment() is the same function used by LibreChat, n8n, and Witsy for safe env isolation
  • PATH merging is untouched — packaged Electron apps still resolve npx correctly
  • User-configured config.env still works as before

References

…P child processes

resolveBaseEnv() started with { ...process.env } and merged the full
login shell environment, passing everything to StdioClientTransport.
Any MCP server spawned by OpenCowork inherited all env vars including
API keys (OPENAI_API_KEY, ANTHROPIC_API_KEY), cloud credentials
(AWS_ACCESS_KEY_ID), and auth tokens.

Changes:
- Import getDefaultEnvironment() from @modelcontextprotocol/sdk/client/stdio.js
- Replace { ...process.env } with { ...getDefaultEnvironment() } which
  provides only safe variables (HOME, LOGNAME, PATH, SHELL, TERM, USER)
- Redact sensitive env logging (was probing OPENAI_API_KEY etc.)
- PATH merging logic preserved for packaged Electron apps

Fixes: OpenCoworkAI#305

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Review mode: initial

No issues found.

Security

  • Safely replaces { ...process.env } with getDefaultEnvironment() from the MCP SDK, preventing child processes from inheriting sensitive environment variables (API keys, cloud credentials, etc.).
  • Logging is also hardened: specific credential keys are no longer probed or logged.

Correctness

  • The shell environment merge (line ~388) and config.env merge (line ~400) remain intact, so user-configured variables and login-shell PATH enrichment still apply.
  • getDefaultEnvironment() returns essential variables per platform (HOME, PATH, SHELL, etc.) — sufficient for typical MCP server operation.

Residual risks

  • Locale/tz: Some MCP servers that rely on LANG, LC_ALL, or TZ may misbehave if those are absent. The shell merge step (macOS/Linux) should provide them; Windows may not. Not considered a blocker given the security trade-off.
  • Brittleness of getDefaultEnvironment: If the MCP SDK changes this function in the future (e.g., adds/removes vars), Open Cowork's behavior changes silently. Consider pinning the SDK version or adding a test to verify the expected minimal set.

Testing

  • No test coverage added/modified. A unit test for resolveBaseEnv that asserts the returned environment does not contain key patterns (API_KEY, AUTH_TOKEN, SECRET, PASSWORD) and includes the expected safe vars would be valuable.

Summary
Review mode: initial
Review policy: advisory — the check reflects automation health/completion only; it does not approve the PR or resolve findings.
Open Cowork Bot

Verifies that resolveBaseEnv does not leak sensitive variables
from process.env (API keys, tokens, secrets, passwords, credentials)
to MCP child processes.

Tests cover:
- No sensitive key patterns (API_KEY, SECRET, TOKEN, PASSWORD, etc.)
- No sensitive values leaked via string matching
- Safe base variables (PATH, HOME) are present
- Total variable count stays minimal vs full process.env

Addresses: Code Review Bot feedback on PR OpenCoworkAI#306
@Correctover

Copy link
Copy Markdown
Author

Good suggestions from the automated review. Added security tests in src/tests/mcp/mcp-env-security.test.ts that verify:

  1. No sensitive key patterns (API_KEY, SECRET, TOKEN, PASSWORD, CREDENTIAL, etc.) appear in the resolved environment keys
  2. No sensitive values leaked via string matching (tests with fake OPENAI_API_KEY, ANTHROPIC_API_KEY, AWS_SECRET_ACCESS_KEY, GITHUB_TOKEN, DATABASE_URL, JWT_SECRET injected into process.env)
  3. Safe base variables (PATH, HOME) are always present for MCP server functionality
  4. Variable count stays minimal - resolved env has fewer keys than full process.env

All 13 tests pass (9 existing + 4 new security tests).

Regarding SDK version pinning - getDefaultEnvironment() is a stable API from @modelcontextprotocol/sdk that returns a minimal safe set (HOME, LOGNAME, PATH, SHELL, TERM, USER). The security property holds regardless of SDK version since the function is designed to be the safe minimal environment. The tests above will catch any regression if SDK behavior changes.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Review mode: follow-up after new commits

Findings

  • [Blocker] Test mock signature mismatch for child_process.execFile — The mock at src/tests/mcp/mcp-env-security.test.ts:44-61 defines the callback with (err, result) where result is an object { stdout, stderr }, but Node's execFile callback expects (err, stdout, stderr) where stdout and stderr are strings. This mismatch means the real resolveBaseEnv will receive an object as the stdout string, causing parse failures or false passes. The test may not actually validate the real behavior.
    Suggested fix:

    vi.mock('child_process', () => {
      const execFile = vi.fn(
        (
          _cmd: string,
          _args: string[],
          _opts: unknown,
          cb: (err: null, stdout: string, stderr: string) => void
        ) => {
          const stdout = [
            'HOME=/home/testuser',
            'SHELL=/bin/bash',
            // ... rest of safe vars
          ].join('\n');
          cb(null, stdout, '');
        }
      );
      return { execFile, default: { execFile } };
    });
  • [Major] Fragile access to private method resolveBaseEnv — The test uses (manager as unknown as { resolveBaseEnv: () => Promise<Record<string, string>> }).resolveBaseEnv to call a private method. If the method is renamed, removed, or refactored, the test will break silently at runtime. Consider exposing the method for testing (e.g., via a package-level export or a test-only subclass) or testing through a higher-level public API (e.g., starting an MCP server connection and inspecting the spawned process environment).

  • No new issues introduced in src/main/mcp/mcp-manager.ts — The changes to use getDefaultEnvironment() and sanitize logging are correct and match the previous review's approval.

Summary

Review mode: follow-up after new commits
Review policy: advisory — the check reflects automation health/completion only; it does not approve the PR or resolve findings.

The core security fix (using getDefaultEnvironment() and safe logging) remains sound. The new test file adds valuable regression coverage but has a critical mock signature mismatch that must be corrected; the fragile private method access is a secondary concern. Address the blocker before merging.

Testing

Four tests added, covering absence of sensitive keys/values, presence of safe vars (PATH, HOME), and reduced env size. The execFile mock mismatch (blocker) must be fixed for these tests to provide reliable coverage.

Open Cowork Bot

- Mock now uses proper Node.js callback: (error, stdout, stderr)
  instead of passing an {stdout, stderr} object
- Use @ts-expect-error instead of unknown cast for private method access
- Extract getResolvedEnv helper to document the private access pattern
- Extract SAFE_SHELL_OUTPUT constant for reuse

These changes address the Open Cowork Bot code review blocking
comment on PR OpenCoworkAI#306.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Review mode: follow-up after new commits

Findings

  • [Fixed] Blocker: Test mock signature mismatch — The child_process.execFile mock now correctly uses the Node.js callback signature (error, stdout: string, stderr: string) and passes SAFE_SHELL_OUTPUT as the stdout string. The previous blocker is resolved. src/tests/mcp/mcp-env-security.test.ts:44-61

  • [Minor] Fragile private method access — The @ts-expect-error approach in the new getResolvedEnv helper (src/tests/mcp/mcp-env-security.test.ts:88-90) still accesses a private member. While the helper provides a single point of change and compile-time checks catch renames, it would be more robust to test through a public API (e.g., by starting a transport and inspecting the spawned process). This is acceptable for now but worth improving in future test refactoring.

No new issues introduced. The mcp-manager.ts changes remain correct and secure. The test coverage is thorough and correctly verifies the security property.

Summary

Review mode: follow-up after new commits
Review policy: advisory — the check reflects automation health/completion only; it does not approve the PR or resolve findings.

The previous blocker (mock signature mismatch) is fixed. The core security improvement in mcp-manager.ts is unchanged and sound. The test file provides strong regression coverage. Minor concern about private method access persists but is mitigated.

Testing

All four tests pass with correct mocks. Suggested additional test: verify that user-provided config.env values are merged correctly into the final environment. Not run (automation).

Open Cowork Bot

Verifies that user-provided config.env is correctly merged with the
safe base environment from resolveBaseEnv:
- User config variables appear in final env
- User config overrides base env for conflicting keys
- Sensitive process.env vars are NOT re-introduced through merging

Addresses: Open Cowork Bot code review suggestion on PR OpenCoworkAI#306.
Total: 16 tests passing (9 existing + 7 new security tests).

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Review mode: follow-up after new commits

Findings

No new issues introduced in this follow-up. The previous blocker (mock signature mismatch) was resolved in the prior iteration, and the current changes only expand test coverage for config.env merging. The three new test cases (user-provided config.env variables are merged, user config.env takes precedence, config.env merging does not re-introduce sensitive vars) are well-structured and correctly verify the security properties of getEnhancedEnv.

The @ts-expect-error approach for accessing private methods remains fragile, as noted in the previous review, but is acceptable for unit testing internal security invariants.

Summary

Review mode: follow-up after new commits
Review policy: advisory — the check reflects automation health/completion only; it does not approve the PR or resolve findings.

The core security fix in src/main/mcp/mcp-manager.ts (replacing process.env with getDefaultEnvironment()) is unchanged and correct. The new test additions provide thorough regression coverage for the config.env merging behavior. No blockers or major concerns.

Testing

All seven tests (four existing, three new) pass with correct mocks. Test coverage now includes both resolveBaseEnv security properties and getEnhancedEnv merge behavior. Not run (automation).

Open Cowork Bot

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Security: MCP server child processes inherit entire process.env including API keys

1 participant