Recently, I was auditing the security boundaries of Visual Studio Code. I wanted to see if a malicious extension could break out of its sandbox and read sensitive files on my computer.

What I found was surprising. I discovered a classic path traversal vulnerability. This exact bug was actually fixed years ago, but it was accidentally reintroduced during a recent code refactor. By using a simple URL encoding trick, I was able to steal GitHub OAuth tokens, SSH private keys, and session cookies.

Let’s walk through how this security boundary works, why it failed, and how an old bug came back from the dead.

TL;DR

I discovered a severe path traversal vulnerability in VS Code Stable 1.123.0.

VS Code uses “Webviews” to display custom interfaces. It restricts these views from reading files outside of specific allowed folders.

By replacing standard forward slashes with their URL-encoded equivalent (%2f), I bypassed the security check. The system saw an allowed folder prefix, but when it fetched the file, it decoded the slashes, resolved the ../ traversal, and handed me the contents of any file on the system. This is a regression of a previously patched vulnerability known as CVE-2022-41042.

What is a Webview?

Before we dive into the exploit, let’s understand the sandbox.

VS Code allows developers to create custom user interfaces using a feature called a Webview. You can think of a Webview as an iframe embedded directly inside the editor. Extensions use them to render Markdown previews or build entirely new panels.

Because a Webview is essentially a mini web browser running JavaScript, it needs strict security boundaries. You do not want a random extension silently reading your personal files.

To solve this, VS Code uses an allowlist called localResourceRoots. This tells the Webview it is only allowed to load local files if they live inside specific folders. If a script inside the Webview asks for a file outside of those roots, VS Code is supposed to block the request.

The Bug: Two Different Parsers

When a Webview requests a file, VS Code checks if the requested file path starts with one of the allowed root paths.

If the allowed root is /workspace/media/, and the Webview asks for /workspace/media/image.png, the system says it is fine and lets it through.

But what if an attacker asks for a file outside that folder using a path traversal? A path traversal uses “dot-dot-slash” (../) to move up a directory.

If the attacker asks for /workspace/media/../../../etc/passwd, a properly written security check will realize that the ../../../ cancels out the root folder. It will then block the request.

However, in this version of VS Code, the security check was just doing a simple string comparison. It did not resolve the ../ segments first.

To exploit this, I requested the following path:

<allowed-root>/x%2f..%2f..%2f..%2fetc%2fpasswd

Notice the %2f part? That is the URL-encoded version of a forward slash (/). Because it was encoded, the first layer of VS Code’s security check did not see it as a directory separator.

Here is exactly how the failure played out:

  1. The Check: VS Code looked at the string <allowed-root>/x%2f..%2f..%2fetc%2fpasswd. It asked if the string started with <allowed-root>/. It did, so the security check passed.
  2. The Fetch: After passing the check, VS Code sent the path deep into the file system logic to actually read the data. This lower-level code automatically URL-decoded the string, turning %2f back into /.
  3. The Escape: The path became <allowed-root>/x/../../../etc/passwd. The operating system resolved those ../ segments, stepped out of the allowed folder, and read the system password file instead.

The Impact: Stolen Keys

This bypass completely destroys the Webview security boundary. Any JavaScript executing inside a Webview can read any file that the VS Code application itself has permission to read.

To prove the impact, I built a Proof of Concept using a Cross-Site Scripting payload. I created a malicious workspace file that injected my JavaScript into the Webview when opened by a benign preview extension.

My script used the path traversal trick to silently read high-value credentials off the disk and send them back to my listener. I successfully stole:

  • The live GitHub OAuth token, granting full access to the victim’s private repos.
  • The Electron cookie store, containing 20KB of active session cookies.
  • The user’s SSH private keys.

Remote Amplification

The vulnerability gets even worse if you use remote development environments.

If a victim opens a malicious workspace using GitHub Codespaces, a Docker dev-container, or Remote-SSH, the Webview communicates with the remote host. When I fired my path traversal payload inside a dev-container, the vulnerability traversed the remote filesystem, not my local one.

This means an attacker could use this bug to steal the internal connection tokens of a cloud dev box. This grants them persistent, authenticated access to your remote infrastructure.

The Reappearing Bug

The most fascinating part of this finding is its history. This exact bypass was originally discovered and patched back in October 2022 as CVE-2022-41042.

The original fix added a normalize() function that correctly collapsed all the ../ segments before checking the prefix. However, in February 2026, a code refactoring commit aimed at using better URI checking accidentally removed those normalization calls. Because there were no regression tests specifically checking for %2f encoded payloads, the automated tests passed. The vulnerability quietly slipped back into the stable release.

The Fix: Normalize First

The fix for this is a foundational rule of secure coding. Always normalize data before you validate it.

Before VS Code checks if a requested path belongs to an allowed root, it must fully decode the URL and collapse all ../ segments. Once the path is in its absolute, final form, only then is it safe to perform the string prefix check. Adding explicit regression tests for %2f and %2e%2e payloads ensures this bug will not rise from the grave a third time.