ProBackend
mfa bypass authentication attacks
1 hour ago8 min read

Vulnerability in VS Code Web Sandbox Exposes Unscoped GitHub OAuth Tokens via Malicious Webviews

Security researcher Ammar Askar disclosed a zero-day vulnerability in github.dev that allows attackers to exfiltrate unscoped GitHub OAuth tokens using the postMessage keyboard shortcut event bubbling of VS Code webviews.

Klaudia Gorski

I have spent years analyzing how identity architectures fall apart, particularly when user convenience clashes head-on with security sandbox boundaries. The recent zero-day discovery in github.dev by computer scientist Ammar Askar is a masterclass in how small features add up to major compromise. By chaining together two helper features—webviews and keyboard event bubbling—Askar showed that an attacker could siphon a developer's unscoped GitHub OAuth token with one simple click.

Let's look at the context here. The github.dev environment is a browser-based, lightweight instance of VS Code that opens whenever you browse a GitHub repository and change the URL from github.com to github.dev or press the period key. Under the hood, GitHub passes an OAuth token to authorize actions on your behalf. This token has unscoped read and write permissions to all public and private repositories you have access to. It is a highly sensitive credential.

When Askar developed a method to extract this token, he didn't exploit a traditional memory corruption bug or high-visibility protocol flaw. Instead, he exploited the UI shortcuts and event-bubbling infrastructure that makes VS Code feel like a native desktop editor inside your browser. The fact that the exploit only requires opening a malicious link (specifically a Jupyter notebook container) means the barrier to attack is exceptionally low. As someone who spends way too much time auditing OAuth scopes, unscoped tokens are my absolute sleep-deprivation trigger. If you're like me and spend your days looking for credential risks, you know that the easiest path is almost always the one that relies on human-like simulation.

The web-based IDE model has grown rapidly over the last few years. Developing in the cloud makes jumping into code instant, bypassing the need to clone repos locally, configure runtimes, or install dependencies. But that ease of access introduces new security profiles. When you open a repository in github.dev, GitHub transmits your active browser session's OAuth credentials to the VS Code instance running in your tab. Because the interface is basically the entire Visual Studio Code application ported to the browser, it is a massive codebase—millions of lines of TypeScript running in the client context. This makes it an incredibly rich target for security researchers wanting to inspect how sandboxing behaves when major features are translated from containerized desktop environments to a web origin.

1. Introduction to the github.dev Zero-Day

2. Root Causes: Unscoped Tokens and Webview Message Overloading

To understand how this attack works, we have to look at the VS Code webview security model. Webviews are implemented via iframe tags pointing to separate origins, such as vscode-webview:// or its browser equivalent. This isolation is designed to prevent untrusted execution environments, like Markdown preview engines or Jupyter math rendering blocks, from calling high-privilege APIs or accessing the primary VS Code workspace assets directly. If a developer loads an interactive widget or JavaScript payload in a Jupyter notebook cell, it executes in this isolated context.

However, developers expect desktop-level keyboard shortcuts to work globally, even if they have focused their cursor inside an active webview container. If you press Ctrl+F to search, or Ctrl+Shift+P to open the command palette, VS Code needs to receive that key event. Because standard browser configurations prevent cross-origin frames from sharing direct keystroke hooks, VS Code uses the Window.postMessage() API to forward keydown events.

Inside the webview, a listener registers keydown inputs and posts them to the host window as a did-keydown message. The host then handles them seamlessly. The architectural flaw here is that the host window does not distinguish between a did-keydown message generated by a physical key press and a did-keydown event programmatically launched via Javascript in the webview. This means a script running in the untrusted sandbox can forge keyboard events and send them up.

By mapping inputs directly to high-level actions, the design prioritizes a natural user flow over strict execution boundaries. If postMessage is treated as a trusted administrative control plane without validating the actual source of the input event, you end up with an authentication bypass. Usually, in web applications, any postMessage interface must validate the origin and verify that data payloads cannot trigger state changes. Here, by allowing key events to bubble up and be interpreted as actual keyboard presses in the parent layout, VS Code accepted simulated commands that it should have rejected. In my red teaming experience, whenever you find an API that bridges a security sandbox just to improve UI sync, you find a bug.

The compromise is worsened by the OAuth architecture. Because the authentication flow provides github.dev with an unscoped token, anyone who can compromise the web editor immediately gains full access to the victim's entire repository footprint. This failure to implement scoped permissions or dynamic privilege escalation is a major identity risk that mirrors other recent issues in cloud environments. For example, similar identity governance and exfiltration dynamics can be explored in our review of cybersecurity evolution and securing autonomous agents.

2. Root Causes: Unscoped Tokens and Webview Message Overloading

3. Technical Walkthrough: Simulating Input to By-pass Trust Safeguards

How does a researcher chain these simulated messages into an actual exploit? Askar published a proof of concept repository showing the flow. The exploit payload relies on opening a Jupyter notebook (README.ipynb) containing an image tag with an onerror handler to execute malicious JavaScript.

Once loaded, the attacker's script initiates a sequence of events:

  1. First, the script waits for VS Code to load. It then simulates a keydown event for Ctrl+Shift+A. In VS Code, this combination triggers the default shortcut 'Notifications: Accept Notification Primary Action'.
  2. What notification is it accepting? The repository includes a .vscode/extensions.json file recommending a local workspace extension located in .vscode/extensions/. When the workspace loads, VS Code pops up a notification recommending the installation of this extension. By dispatching Ctrl+Shift+A, the script automatically clicks 'Install' for the workspace extension.
  3. Why not install a global marketplace extension instead? VS Code 1.97 introduced a 'publisher trust system' that displays a strict confirmation dialog when installing public extensions for the first time. However, local workspace extensions are loaded directly from the repository's .vscode/extensions folder. As long as the workspace is trusted (which github.dev workspaces are by default), local workspace extensions bypass this publisher trust prompt.
  4. Once the local extension is loaded, its custom properties contribute keybindings. Askar configured the malicious extension to contribute a keyboard shortcut—Ctrl+F1—which maps to running the native command workbench.extensions.installExtension. The extension's package.json specifies arguments to install a second extension that actually performs the exfiltration, explicitly passing donotSync: true and a context object skipping the publisher trust check.
  5. Finally, the script dispatches a simulated keydown for Ctrl+F1. The keyboard handler interprets this as a legitimate user command, installing the second, highly permissive extension.

This second extension runs inside the full VS Code extension host environment, where it has access to the active tokens. It reads the GitHub API token associated with the session, calls the GitHub user repository endpoints (api.github.com/user/repos), and exfiltrates the credential. A developer who simply clicked a link to view a repository in github.dev now has their entire private codebase exposed.

The repository structure is deceptively simple. In the PoC, the .vscode directory contains extensions.json containing the recommendation AmmarTest.hello-ammar-github. Additionally, the .vscode/extensions directory hosts the extension files, including its package.json and compiled extension script. When a developer visits the github.dev link, the browser reads README.ipynb and renders the Jupyter markdown cell. Inside the cell, the markdown parser converts the image tag <img src="data:foobar" onerror="..."/> into HTML, triggering the error event immediately since the source is invalid. This fires the JavaScript wrapper, executing the key simulation routines. Notice how the steps align: first the initialization delay, then the simulated keydown for the primary action to accept the workspace extension notification, and finally the custom shortcut trigger. It is an orchestrated loop that executes cleanly without requiring any manual user interaction beyond clicking the initial URL.

4. Response, Mitigations, and the Disclosure Controversy

Microsoft has deployed mitigation measures on the service side to address this. However, developers who have previously authenticated on github.dev and have cached credentials stored in their browser could still be at risk if the cookies and local site data are not cleared. Ammar Askar recommended that developers clear local storage and cookies specifically for the github.dev domain. Doing so prompts the initial OAuth warning popup again, preventing silent authorization. If you haven't cleared local data, malicious redirects on the web could theoretically execute the attack against active sessions.

The disclosure of this zero-day has also reignited discussion around Microsoft's security handling. Askar opted for full public disclosure because of his prior interactions with the Microsoft Security Response Center. In a previous report, MSRC patched a VS Code bug silently, refused to catalog it as a security vulnerability (denying a CVE), and provided no researcher credit. Askar's decision to bypass private reporting highlights the growing tension in the researcher ecosystem regarding how vendor programs manage disclosure.

For organizations, this incident highlights the danger of relying blindly on sandboxed frames when trust states can be bridged via keyboard emulation channels. Developers should treat web-based IDEs with the same zero-trust skepticism applied to desktop applications, and maintain hygiene over their browser-level OAuth tokens. Similar risks of token theft and context breakouts are discussed in our analysis of prompt injection in Copilot.

Security companies like StarLabs have reported similar frustrations when dealing with VS Code. In a recent disclosure, StarLabs found an XSS bug in VS Code that Microsoft marked as ineligible for their bounty and characterized as low severity. This pattern of classifying severe client-side execution paths as low impact frustrates the security community. When researchers spend dozens of hours mapping exploit chains only to see them closed as 'non-security boundaries' or patched without CVE advisories, public disclosure becomes the path of least resistance to force remediation. By releasing the PoC publicly on GitHub, Askar ensured that the danger could not be downplayed. As teams continue to migrate their developmental pipelines to browser-based instances, managing authentication tokens will remain a key defense checkpoint. Protecting the developer workstation starts with treating the browser session as a high-value boundary.

More blogs