This is the full story of the vulnerability we have discovered within Visual Studio Code (VS Code) concerning the handling of secure token storage. While designed for isolated storage for each extension, this vulnerability presents a high-risk “Token Stealing” attack. A malicious extension could expose third-party application tokens “securely stored” by your VS Code IDE, posing significant risks to entire organizations.
Developers should be cautious with VS Code extensions that integrate with third-party applications, as these extensions store tokens in the IDE storage, making them vulnerable to potential theft by malicious extensions. Staying aware of this risk is essential to ensure the protection of both personal and organizational security.
In the world of software development, Visual Studio Code (VS Code) has become a popular and powerful code editor known for its wide range of extensions. These extensions provide developers with extra tools and features to enhance their coding experience. These extensions often rely on tokens to enable seamless integration with external services and APIs. However, our findings reveal the existence of a hidden security vulnerability that presents a significant risk to users and organizations utilizing extensions with tokens.
In VS Code, extensions store tokens provided by developers to integrate with third-party systems. VS Code offers a secure and isolated solution for storing these tokens within the operating system. However, our research uncovered a new attack vector in this functionality. We developed a proof-of-concept malicious extension that successfully retrieved tokens not only from other extensions but also from VS Code’s built-in login and sync functionality for GitHub and Microsoft accounts, presenting a “Token Stealing” attack. This vulnerability extends beyond individual computers and poses a potential risk to organizations, as these tokens often belong to organization user accounts. Our findings highlight the urgent need for enhanced security measures and greater security of extension permissions within the dynamic VS Code ecosystem.
In this article, we explore this security loophole in VS Code and its implications for users relying on them. We provide a detailed account of our journey, starting with a demonstration of our initial proof of concept (POC). Then, we delve into the technical aspects of the exploit and highlight an additional bug we discovered. After explaining the significance of this vulnerability, we give suggestions on how to protect your systems.
Let’s uncover the depths of this issue together and ensure the security of your organization’s valuable resources.
The Building Blocks of VS Code Extensions
Designed to augment the editor’s functionality, VS Code extensions primarily rely on two core files: extension.js
(or extension.ts
) and package.json
. The extension.js
or extension.ts
file controls the extension’s behavior, including interacting with the VS Code API. package.json
defines the extension’s metadata and permissions, as well as when and how the extension is loaded in the editor.
As part of the VS Code environment, extensions have a set of privileges that allow them to interact with the system. They can read and write files, execute commands, and access secrets stored by VS Code in their context, depending on the API they’re using and the permissions granted to them.
Modifying Extensions Code Using a Dummy Extension
Let’s consider a scenario where we create a basic dummy extension whose sole purpose is to modify another extension’s code. In this case, we’ll look at the CircleCI extension – a popular CI system. The objective is to alter the uglified extension.js
code of CircleCI to print out the token.
In the code snippet from CircleCI’s extension, we incorporate a command that reveals the token. Alternatively, this command could be programmed to send the token to our server.
Reload VS Code, and then in the developer’s console:
Voila! 💃
Though it sounds straightforward, it’s important to note that this method would require specific adjustments and research for each extension targeted due to differences in their respective codes. Furthermore, the malicious extension must patch the target extension’s code per version, considering that updates might change the code structure.
While this method is theoretically possible, it’s not particularly efficient or reliable. The need for constant code adjustments and version-specific patching makes it less than ideal for broad exploitation. In the next section, we explore a more versatile approach.
Let’s Scale Up
Our purpose was clear: Discover a method that could allow the extraction of secrets without tampering with an extension’s source code. We aimed for a stealthy approach – silent, clean, and efficient. The open-source nature of VS Code was our starting point, and it prompted us to clone the code and delve into its intricate workings.
Essential Terms to Know Before We Dive In
Extension Context
In VS Code, the context of an extension defines its operational environment. It is the scope within which the extension communicates with the VS Code API, leveraging a collection of APIs and resources. The context allows extensions to react to events, contribute functionality, and preserve a state through restarts.
Secrets Storage (secretStorage)
VS Code provides extensions with a Secret Storage API, a safe, centralized hub for managing sensitive data. Each extension is assigned a secretStorage
object tailored to its context, commonly used to hold authentication tokens or other confidential data.
Keyring and Keychain
The keyring in Linux and the keychain in macOS are encrypted storage systems provided by the operating systems. They store passwords and other secrets, protecting them in a secure and standardized manner. VS Code uses them to save the encrypted tokens.
Keytar
Keytar is a Node.js wrapper for system keychain services. It provides an asynchronous API that lets applications read, write, and delete passwords from the system’s keychain, interfacing with the native C++ library to make system calls. In VS Code, Keytar is often used for storing and retrieving secrets. You can read more about Keytar here.
A Deep Dive Into VS Code Secrets
To start off, we began by creating our own extension and utilized the secretStorage
to securely store a secret. During this process, we familiarized ourselves with the concept of extension context and discovered that the secretStorage
is assigned per context. Curious to explore further, we attempted to switch to another extension’s context from within our running extension, only to realize it was impossible.
Our next idea was to inject code into a universal spot within the package of every extension. However, this tactic didn’t deliver a satisfying solution. Consequently, we have cloned the entire VS Code project. We did this to gain a deeper understanding of the mechanics behind the creation of secretStorage
and the determining characteristics of an extension’s context.
Upon examination, we found that the context is returned as a frozen object from the loadextension
function. Thus no properties manipulation was possible on the context object in order to fool VS Code to give us other context’s secretStorage
.
We traced the implementation of secretStorage
up to its point of interaction with another class—KeytarNodeModuleFactory. This class operates intelligently, assessing the operating system and storing secrets in the system’s secret storage using Keytar module. For Apple users, the secrets are securely stored within the system’s keychain.
With this knowledge, we opened the keychain and sure enough, discovered VS Code’s secrets stored within:
But what we saw next raised an eyebrow. Under the access control tab, the application Visual
Studio Code was granted access to this secret. In other words, this means that any extension, even a potentially malicious one, could access this secret, given that they are run by VS Code
In our extension, we retrieved the passwords using Keytar and found they were encrypted, leading us to a quest to understand the encryption implementation. After studying the VS Code’s source code, we found two meaningful insights.
First, the decryption takes place using the secret and the machineId
. The machineId
is a unique identifier for your system.
And second, the encryption mechanism takes place using vscode-encrypt
module.
Navigating the filesystem, we located the vscode-encrypt
module under /Applications/Visual Studio Code.app/Contents/Resources/app/node_modules.asar.unpacked/vscode-encrypt/build/Release/vscode-encrypt-native.node
and imported it into Node. Initial attempts to decrypt the secret within Node, however, turned out to be fruitless, yielding output in gibberish.
Determined to unravel the mystery, we debugged the decryption process in VS Code’s Open Source Software (OSS), in order to validate its functioning as we assumed it does.
We were also able to locate the source code of vscode-encrypt
within the app’s Node modules at /Applications/Visual Studio Code.app/Contents/Resources/app/node_modules.asar
. The decryption mechanism was written in the Rust language, then it got compiled into Node native binary that could be imported by VS Code.
This discovery unveiled that the encryption algorithm being used is aes-256-gcm
, and the key for the encryption process included the hash of the current executable path. This discovery clarified why the value could be accurately decrypted when run through VS Code, while running the same through Node resulted in nonsense.
Encouraged by this discovery, we have written a script in JS that imports the vscode-encrypt
module and decrypts the secret. Then we ran it with VS Code’s Electron.
It worked! 🙂
The final step was to develop a POC extension. This POC initially accessed a list of all extensions registered in the VS Code environment. It utilized the findCredentials
function from Keytar to retrieve all VS Code passwords from the keychain. Then it ran the decryption JS script using VS Code’s Electron:
ELECTRON_RUN_AS_NODE=1 "${electronPath}" --ms-enable-electron-run-as-node "${vscodeDecryptScriptPath}" ${machineId}
The script imported the vscode-encrypt
module, and using the machineId
it decrypted the secret and printed the password.
The final step involved forwarding these decrypted secrets to a predetermined IP address associated with a potential attacker.
A disclaimer: If VS Code is unable to import the vscode-encrypt
module successfully, it defaults to storing passwords in plaintext JSON format in the keychain, so the decryption part is not necessary.
With that, We’ve unlocked all your secrets. In the spirit of transparency, We’ve just revealed one of our own.
Additional Bug
VS Code’s logic for assigning the secrets storage for a specific extension’s context presents a notable security concern. The function getFullKey
retrieves secrets by a given extensionId
.
The extensionId
is simply derived from the combination of the publisher
and extension_name
, both defined in the package.json
file and further transformed to lowercase.
Given this scenario, we have modified the publisher
and name
fields of our locally developed extension in its package.json
file. This tricked VS Code into believing that our extension was, in fact, another extension (in this case, CircleCI) and thus granted our extension access to CircleCI’s secretStorage
.
Theoretically, this process could be replicated with numerous other extensions, creating many deceptive extensions that gain access to the secretStorages
of other extensions, hence leading to a security loophole.
VS Code Extensions Best Practices
Through this exploration, we’ve gained some insights about secure extension handling and token storage within VS Code. Here are a few best practices developers and organizations should follow:
- Review Extension Permissions: Before installing an extension, review the permissions it requests. Be cautious if an extension requests excessive permissions that are not directly related to its intended functionality.
- Review Extension: Before installing an extension, if possible, review the source code or at least verify the credibility of the publisher to ensure the extension doesn’t contain any malicious code.
- Limit Extensions: Reduce potential attack vectors by limiting the number of extensions in use. More extensions mean more potential vulnerabilities.
- Encryption: If your extension uses tokens or other sensitive information, consider adding an extra layer of encryption for added security.
Remember, while extensions improve productivity, they also increase the surface area for potential attacks. Hence, always take the necessary precautions while developing or using them.
Disclosure
Microsoft acknowledged the potential vulnerability we disclosed in VS Code, stating it was an inherent result of their design choice not to support extension sandboxing. Hence no fix will be released for this issue.
Summary
Secrets are everywhere – in code, builds, published artifacts, production buckets, and even VS Code extensions.
Developers have been the target of cyber attacks dozens of times before because of the wealth of information they are exposed to, including sensitive tokens and secrets that connect them to the rest of the organization’s infrastructure.
Our article demonstrated a new attack vector by which attackers could escalate their privileges into sensitive assets, including the source code of the entire organization, the build system, and even the cloud.
This is another security issue organizations should address.
How Cycode Can Help
Cycode helps find and fix hardcoded secrets, prevents new hardcoded secrets from being introduced, and reduces the risk of exposure by immediately scanning leaked code for secrets. Cycode offers robust, continuous hardcoded secret detection that identifies any type of hardcoded secret anywhere in the SDLC – whether created by humans or machines.
Learn more about how Cycode can help you detect secrets now at www.cycode.com.
Originally published: August 7, 2023