Harden MCP HTTP endpoint + fix start_debugging hangs for Testing-API runners#78
Merged
Conversation
✅ Extension Build Successful!📦 VSIX artifact is ready for download Scroll down to the "Artifacts" section and download To install: In VS Code, run |
This was referenced Jun 4, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes (1) unauthenticated debugger access via 0.0.0.0 bind and DNS rebinding via missing Host header validation, both reported by MSRC against
src/debugMCPServer.ts, and (2) two bugs in thestart_debuggingflow for VS Code Testing-API runners (notablydotnet test) where the tool would hang past a breakpoint hit and past clean test completion.Part 1 — MSRC: Harden MCP HTTP endpoint
Background
The DebugMCP HTTP server exposes powerful, unauthenticated debugger primitives (
evaluate_expression,start_debugging, …). MSRC reported two composable vulnerabilities:app.listen(this.port, callback)was called with no host argument, so Node bound to all interfaces (0.0.0.0). Any host on the same Wi‑Fi / co‑working LAN couldPOST /mcpand invokeevaluate_expression('__import__("os").system(...)')for RCE on the debuggee, orstart_debuggingagainst a UNC path.attacker.example(short‑TTL DNS flipped to 127.0.0.1) can reach the local server via the victim's browser. The server didn't inspect theHostheader, so the rebound request hitstart_debuggingwith a UNCfileFullPathand spawned attacker‑controlled code under the victim's user. PoC video from the finder confirmed it in Firefox.Fixes
127.0.0.1instead of all interfaces, so it is unreachable from other hosts on the network.Hostheaders. Requests whoseHostheader doesn't name a loopback address (localhost,127.0.0.1,[::1], with or without port) are rejected with HTTP 403 before any MCP handler runs — neutralizing DNS‑rebinding attacks even if the bind is later widened.Originheaders. WhenOriginis present (browser‑originated requests), it must also be loopback; absentOriginis allowed so non‑browser MCP clients keep working.debugmcp.bindHostsetting lets advanced users widen the bind, with a marketplace warning describing the risk and a startup log warning when the value is non‑loopback.POST /mcprequests with attackerHost/Originheaders receive 403.bindHost, and theHost/Originallow‑list, so the documented contract matches the implementation.Manual repro against the fixed build
Rebinding-style request — now rejected:
LAN-style probe — connection now refused:
Part 2 — Fix
start_debugginghangs for Testing-API runners (e.g.dotnet test)Background
When
start_debuggingis called with atestName, the handler routes through VS Code's Testing API (testing.debugAtCursor) so language extensions can manage runner-specific orchestration (e.g.dotnet test --filter <name>spawning atesthost.dllchild process for xUnit/NUnit/MSTest). Two bugs were observed against a .NET xUnit project (Calculator.Tests):debugTestAtCursorawaitedvscode.commands.executeCommand('testing.debugAtCursor'), but that command's promise resolves only when the test run completes, not when the debug session starts. When the test paused on a breakpoint, theawaitblocked the handler forever — thereadyPromisehad already resolved'stopped'but the handler never reachedawait readyPromise. The MCP tool only returned when something external (e.g. the next MCP request) caused the run to wrap up.onDidTerminateDebugSessiondid not reliably fire for both the parent andtesthostchild sessions in time, sowaitForDebugSessionReadyran out the configured timeout (default 180s) even though the test was clearly done.Fixes
IDebuggingExecutor.debugTestAtCursornow returns{ started, runComplete }. The implementation no longerawaitstesting.debugAtCursor; it surfaces the command's promise so the caller can use it as an independent "test run finished" signal. Dispatch errors are still surfaced (logged) via.catch.readyPromiseagainstrunCompletein the handler.handleStartDebuggingnow doesPromise.race([readyPromise, runComplete.then(() => 'terminated')])for the test path. A breakpoint hit returns immediately as'stopped'; a clean run returns immediately as'terminated'. The configured timeout is no longer a wait floor for either case.coreclrno-build / missing-assembly errors clearly. The launch-config branch already threw a descriptive error when no built DLL was found; this PR's tests guard the contract.Regression matrix tests —
src/test/startDebuggingMatrix.test.ts44 new fully‑mocked tests covering both code paths × four scenarios × seven languages. The mocks use
deferred<T>()so each test deterministically controls whenreadyPromiseandrunCompletesettle — no timers, no flakes.testName)testName)readyPromise → 'stopped'→ "stopped at breakpoint"runComplete→ "stopped at breakpoint" (guards bug #1)readyPromise → 'terminated'→ "ran to completion"runCompletewins against a never-settlingreadyPromise→ "ran to completion" (guards bug #2)startDebugging → false→ "Failed to start"debugTestAtCursorthrows → propagates "Could not locate test"getDebugConfigthrows → propagates "Could not find a built assembly" (C# only)Languages covered: Python, JavaScript, TypeScript, Java, C#, C++, Go. Plus one race tie-breaker test ensuring a
runCompleteresolution afterreadyPromise → 'stopped'cannot retroactively change the outcome.Test housekeeping
Removed stale compiled artifacts under
out/test/that had no corresponding.tssource (debugConfigurationManager.test.jsand thee2e/folder) — they referenced API surfaces removed in earlier refactors and accounted for all 20 previously failing tests. Full suite now: 66 passing, 0 failing.Validation
npm run compile✅npm run lint✅npm test✅ — 66 passing / 0 failing (after Inno Setup mutex from a stuckCodeSetup-stable-*installer was cleared)