# CVE-2026-30615 — Windsurf zero-click MCP-config auto-load

| | |
|---|---|
| **Advisory** | [NVD](https://nvd.nist.gov/vuln/detail/CVE-2026-30615) · [Tenable](https://www.tenable.com/cve/CVE-2026-30615) |
| **Affected** | Windsurf (versions prior to the 2026-04 patch) |
| **Patch status** | Fixed in Windsurf latest |
| **agent-airlock preset** | `windsurf_cve_2026_30615_defaults()` (v0.5.7+) |
| **Module** | `agent_airlock.mcp_spec.zero_click_config_guard` |

## What broke

An attacker-controlled HTML page **rewrote** Windsurf's
`.windsurf/mcp.json` from prompt injection alone, with no user
interaction. Windsurf auto-launched the new STDIO server entry on
the next session.

The class generalises beyond Windsurf to **any** IDE that
auto-reads project-local MCP config — VS Code, Cursor, Claude Code,
JetBrains all do this. Anthropic punted; Windsurf patched.

## Threat model

Two distinct diff shapes the guard catches:

1. **New unsigned server entry.** Attacker adds a fresh
   `mcpServers` entry pointing at a binary they control. No signer
   field, or a signer the host doesn't trust.
2. **Mutated command on existing entry.** Attacker doesn't add an
   entry — they flip the `command` array on an existing one to
   point at a malicious binary. Subtler than (1) because the entry
   name doesn't change.

## How agent-airlock blocks it

```python
from pathlib import Path
from agent_airlock import (
    UnsignedMCPServerAdded,
    MCPCommandMutationDetected,
)
from agent_airlock.policy_presets import windsurf_cve_2026_30615_defaults

cfg = windsurf_cve_2026_30615_defaults(
    signer_allowlist=frozenset({"sre-team"}),
)

# Before each tool invocation, audit the current config file:
report = cfg["audit"](
    Path(".windsurf/mcp.json"),
    old_sha256=stored_sha256,  # from the last clean audit
    new_content=current_bytes,
    old_content=stored_bytes,  # optional — enables command-mutation detection
)
# Raises UnsignedMCPServerAdded or MCPCommandMutationDetected on either shape.
```

`audit()` is **diff-on-demand** (synchronous). It does not run a
file-system watcher. The v0.5.8 roadmap covers the long-running
daemon variant; the v0.5.7 contract is "call the guard before each
tool invocation."

## Watched paths (default)

`ConfigFileWatchPolicy.watched_paths` defaults seed the eight known
IDE-host locations:

- `.vscode/mcp.json`
- `.cursor/mcp.json`
- `.windsurf/mcp.json`
- `.claude/mcp.json`
- `~/.config/claude-code/mcp.json`
- `~/.cursor/mcp.json`
- `~/.config/Code/User/mcp.json`
- `~/.windsurf/mcp.json`

Callers extend this list by passing their own `watched_paths`
tuple to `ConfigFileWatchPolicy`.

## Signer allowlist

Empty allowlist means *"any non-empty signer is acceptable"* — a
deliberate choice so integrators can adopt the guard in stages.
Tighten by passing `signer_allowlist=frozenset({"sre-team"})` to
require an explicit identifier.

## Integration with VS Code 1.112 sandboxing

[VS Code 1.112 (2026-03)](https://code.visualstudio.com/updates/v1_112)
shipped MCP server sandboxing on macOS/Linux via the
`"sandboxEnabled": true` flag in `mcp.json`. agent-airlock's
zero-click guard is complementary: VS Code's sandbox restricts
*what* the server can do once running, while this guard decides
*whether* it should run at all.

A future `airlock audit` CLI (queued for v0.5.8) will cross-check
both — flag any `mcp.json` whose entries lack `sandboxEnabled` AND
have no trusted signer.

## Errors

Both subclass `AirlockError` and are top-level re-exports:

| Error | Raised when |
|---|---|
| `UnsignedMCPServerAdded` | New `mcpServers` entry with no trusted signer |
| `MCPCommandMutationDetected` | Existing entry's `command` array changed shape |

## Primary sources

- [NVD CVE-2026-30615](https://nvd.nist.gov/vuln/detail/CVE-2026-30615)
- [Tenable](https://www.tenable.com/cve/CVE-2026-30615)
- [VS Code 1.112 release notes](https://code.visualstudio.com/updates/v1_112)
- Fixture: `tests/cves/fixtures/cve_2026_30615_windsurf_zero_click.json`
