Skip to content

Webview companion server binds to all network interfaces; session secret leaks via browser process args #258

Open

Description

@carfeii

Affected versions: confirmed on commit 96c26e5388eda9ed81a488f1d95a26a9af166214 (main branch)

Summary

Every time the Debug Visualizer webview is opened, the extension starts a companion HTTP + WebSocket server (WebviewServer) that binds to all network interfaces instead of localhost, with no configuration option to restrict it. The WebSocket channel exposes an RPC method (setExpression) that evaluates arbitrary expressions in the active debug session, gated only by a per-session secret. That secret is passed to a system browser as a URL argument when the extension's "open in browser" / Chrome kiosk feature is used, which places it in the browser process's command line, readable by any local user via ps//proc/<pid>/cmdline. Combined with the server listening on all interfaces rather than loopback, any other user on the same machine, or any other host reachable on the local network, who obtains the leaked secret can connect to the WebSocket RPC and evaluate arbitrary expressions inside the victim's live debug session.

Details

extension/src/webview/WebviewServer.ts calls:

const app = express();
app.use(serveStatic(distPath));
this.server = app.listen();        // <-- no host argument

const wss = new WebSocket.Server({ server: this.server });

http.Server.listen() / express().listen() with no host argument binds to all interfaces (0.0.0.0 / ::), not 127.0.0.1. There is no debugVisualizer.* setting anywhere in extension/src/Config.ts or package.json that lets a user restrict this to loopback.

Static files under distPath (the webview UI bundle, index.html, main.js) are served unauthenticated via serveStatic.

The WebSocket RPC (WebviewConnection.ts) gates its interesting methods behind a secret:

authenticate: async ({ secret }, { newErr }) => {
    if (secret !== serverSecret) { return newErr(...); }
    else { authenticated = true; }
},
setExpression: async ({ newExpression }) => {
    throwIfNotAuthenticated();
    ... // evaluates newExpression in the active debug session
},

serverSecret is a 30-character cryptoRandomString generated once per VS Code session in WebviewServer.ts. It is not kept purely in-process: when the user invokes "open in browser" with debugVisualizer.useChromeKioskMode enabled (default: true), the extension calls chromeLauncher.launch({ chromeFlags: ["--app=" + url] }) where url contains serverSecret in its querystring. This puts the secret in the spawned Chrome process's command line, readable by any other local user on a shared machine via ps aux or /proc/<pid>/cmdline. Once the secret is known, the all-interfaces bind means it is exploitable from anywhere on the local network, not only from the same host.

Steps To Reproduce

(available upon request)

Impact

A background server tied to the user's live debug state is reachable from the entire local network rather than loopback only, for the duration of every Debug Visualizer session, with no way to restrict it. Any other local user on a shared machine who reads the per-session secret out of the Chrome process's command line, or any network-adjacent attacker who otherwise obtains that secret, can evaluate arbitrary expressions inside the victim's live debug session over the network, reading or mutating state in the process being debugged. Even without the secret, the server leaks the extension's unauthenticated static webview bundle/version to the whole network for the lifetime of every session.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions