There are probably better sources but I think this video by The Primeagen is a good introduction.
Github creds or the computer, can't decide which one is worse.
Zooming way out (perhaps to the point of useless observation), it's a pity that the web embedded VSCode editor is signed into GitHub at all. Defense-in-depth or not, a huge vulnerability surface arises from that original sin. It'd be like if you had a god-permissioned GitHub API token stored in world-readable plaintext on your workstation for the malicious-NPM-package-of-the-week to find.
In a perfect world, it'd be awesome if the in-browser IDE launched with a temporary per-repo permission scope or token that allowed only pull and push to the repo in question; no github.com web session whatsoever. If you want the full GitHub web UI experience, well .... go back to github.com; make github.dev a single-repo service.
I'm assuming that's a) inconvenient for users, b) hard to implement, and c) a historical assumption baked into a lot of the github.dev tooling, though. Ah well.
github token got stolen and also cloudflare tokens
guys even if you take security seriously you are going to get hit on a long enough time frame
best thing to do is segregate and control damage
trust no one, nothing, use orbstack, and always operate under the assumption that your token is going to get leaked at some point
it knocked off my entire momentum. fortunately seemed like it was just a spam bot that took my tokens and created bunch of fake spam pages and trying to mine crypto
the biggest feeling is the one of feeling violated
take care fellow travelers
Classic MSRC. It has figured out that researchers will report for free regardless. Why change?
The author said:
You cannot just use the shortcut trick to install the evil extension directly because of new publisher trust system;
You can bypass this by using local workspace extensions which has no publisher screening, but CSP blocks it;
The solution seems to be that installing a local workspace extension which binds a shortcut of 'install extension without checking publisher'.
So I assume it means:
1. you need two extensions, 1st one is local and only for the keybinding, and 2nd one is the 'real' evil one and it doesn't need to (actually can't, because of CSP) be local anymore?
2. the CSP only prevents the JS in local extension but nothing about its package.json (or the ability to add shortcuts), right?
That's actually exactly what they do for codespaces. The token only has read/write on the repo you activated for the codespace [1]. They should definitely consider doing that for github.dev as well.
[1] https://orca.security/resources/blog/hacking-github-codespac...
We can try to just put a `my-extension/extension.js` for the most direct execution but the CSP blocks that. It's only a script-src CSP blocking it though, so fetching the package.json is still kosher. So we end up using it to contribute a keybinding instead.
I have no interest in selling these vulnerabilities or sitting on them. At the same time, it feels really bad to have a vendor disrespect the hours it can take to make a proof-of-concept by just patching it silently and not crediting you or acknowledging it.
Someone is going to be blacklisted by Microsoft.
How about pull from the repo but only push to a staging area from which the user, but not the token, can push for real?
Frankly, LLM agents should do this too. Letting your LLM push seems foolhardy to me.
> created bunch of fake spam pages and trying to mine crypto
Pages like GitHub pages? We’re repos being created in your account? Curious how you discovered that your tokens were pwnedI don’t know the specifics of this case, but I’ve managed bug bounty programs in the past through Bountysource and HackerOne. One thing that occasionally happens is that a report makes its way to the development team before the security team has fully assessed it, in this case MSRC.
At that point, a developer may decide to quietly fix the issue. Sometimes that’s driven by a concern, rational or not, that being associated with a security bug could reflect poorly on them or affect future promotion opportunities. The result is that by the time the security team attempts to reproduce the report, the vulnerability is already gone.
From MSRC’s perspective, all they see is that the provided reproduction steps no longer work. They have no visibility into the internal history of the bug or whether someone already patched it. As a result, the report gets closed as invalid even though the original finding may have been legitimate.
Percolating...
Ban all vulnerability researchers
I have been thinking more and more about how I might use this pattern.
saw a weird spam site, so damn tired went to bed thinking it was some mislick on my side
woke up next morning and loaded up my domain, it redirected and panic set in
my SEO is probably nuked even though it has been under 24 hours
But then someone else on the team should have to manually approve that MR to allow it to be merged to main.
This kind of defeats the ability of malware to push stuff out automatically.
This is laziness, security absolutely could verify these steps.
I'm catching up on the infosec twitter side but it seems like it was even worse. A lot of people have the same story as me in 2023 of "they silently patch the bug and don't even credit you" which really stinks.
I hope you get credit where credit is due in future endeavors.
Small nitpick, but it's also possible to communicate by changing the location.anchor property (by either the iframe or its parent window.)
Maybe it’s just my preference, but I like having a small setup where I know what is installed and what is running. With VSCode, browser IDEs, extensions, sync, tokens, and random plugins, it gets hard to tell what actually has access to what.
I wish I could add “and I never looked back”, but honestly in the past year or two Neovim started regularly breaking my setup (approximately every upgrade). Had some inklings it might happen eventually… Strictly speaking, 10 years in, nvim is yet to have its first stable version released—which means technically one can’t blame it for instability, but which is useful to keep in mind.
Considering going back to plain vim. I’m sure I will lose many niceties, but hopefully it would not require me to troubleshoot broken functionality in the middle of work.
Even with network monitoring, exfil to Github itself can be very hard to stop unless you SSL intercept and have very strict URL allow lists.
Best is to move away from Github, move to self hosted internal Gitlab/Forgejo and block Github completely.
Depending on what third party packages you use, you may sometimes get breakage there, but if you start out with a kit like doom emacs, you’ll be largely insulated from that.
There’s also always newer stuff like zed, which looks pretty great and is very snappy in my limited testing.
https://doublepulsar.com/microsofts-stance-on-zero-day-explo...
Just by clicking a link, it’s possible for an attacker to steal a GitHub token that can read and write to your repos, including private ones.
Did you know GitHub has this really cool feature called github.dev?
On any repository you have access to, if you can change the url from github.com to github.dev or you click this little menu item:

You’ll be launched into a little light-weight version of VSCode that runs entirely in your browser (I guess that’s one advantage of having your app written with electron).

This browser instance of VSCode is pretty powerful, you can view all the files in the repo (even if it’s a private one), you can send out pull requests and even make commits.
This functionality is achieved by github.com POSTing over an OAuth token to github.dev that allows it to interact with GitHub on your behalf. The token is not scoped to the particular repo you interacted with, meaning it has full access to every other repo that you have access to.
The presence of this token and the fact that this web-app is running almost the entire brunt of VSCode’s million line Typescript codebase makes it a great target for anyone looking into VSCode bugs. That sort of bug is what we’ll explore here and show how an attacker can use it to exfiltrate your GitHub token.
Being an electron app on the desktop, executing arbitrary Javascript inside of VSCode would be tantamount to full remote code execution. This is why VSCode implements some sandboxing approaches, the one we’ll focus on here is VSCode’s webviews.
Webviews use an <iframe> with a different origin to the main VSCode window to ensure that any JavaScript executed inside of them is fully isolated. These webviews are used for features such as Markdown previews or editing Jupyter notebooks:

The output of the cell is rendered into an <iframe> from the origin vscode-webview://..., as opposed to the main electron window which has the origin vscode-file://.... This means that even if the Jupyter notebook uses the built-in features of displaying HTML or using Javascript for interactive widgets, the actual core VScode application is protected from it. One cannot use Electron’s integration with Node.js APIs inside this iframe or call into VSCode’s APIs from this frame.
Great, that gives us the ability to render content, but just static content is boring. How do we implement features like having the Markdown preview show you which source line you currently have highlighted or updating the preview live as we edit it?

The same cross-origin policy that gives us security also prevents our main editor window from interacting with the DOM in the vscode-webview://... frame. After all, you wouldn’t want someone who used an <iframe src="google.com"> to be able to interact with the google page to steal your cookies or change that website’s behavior.
> document.getElementsByTagName('iframe')[0].contentWindow.findElementById('foo')
Uncaught SecurityError: Failed to read a named property 'findElementById' from 'Window':
Blocked a frame with origin "vscode-file://vscode-app" from accessing a cross-origin frame.
The only way to allow this behavior is to have the two web pages in the different origins cooperate with each other using the Window.postMessage() API. This method allows sending JavaScript objects across the different windows. So in that example of showing which rendered Markdown line corresponds to what editor line, the main editor window posts a little message like this:
{
type: "onDidChangeTextEditorSelection",
line: 31
}
and then the corresponding code running inside of the webview has a listener for this message that adds the highlight:
window.addEventListener('message', async event => {
const data = event.data as ToWebviewMessage.Type;
switch (data.type) {
...
case 'onDidChangeTextEditorSelection':
marker.onDidChangeTextEditorSelection(data.line, documentVersion);
return;
Note: VSCode in the browser uses a similar sandboxing model. VSCode developer Matt Bierner has a great blogpost about the challenges of porting it over from Electron worth checking out.
So our security boundary for webviews roughly looks like this:

but in terms of UI, our webview sits right here in the window. People expect basic things like clicking links, drag and or pressing Ctrl+F to work inside of them:

Hence, VSCode implements a bunch of basic functionality through the message passing mechanism to enable these features. Speaking of keyboard shortcuts, the astute reader who has dealt with <iframe>s may have already picked up on the issue.
As with most things cross-origin, the browser offers a good amount of isolation between the two frames. If you had a page on hackerman.com and you iframed google.com/login, you would not want the hackerman page to be able to attach a keyboard listener onto the iframe. That would let them see all your keystrokes on google.com, allowing them snoop your password.
Okay given that information, try clicking inside a VSCode webview and then pressing Ctrl+Shift+P to bring up the command palette.

Oh yay, that works. Wait. Oh. Oh no. So, to avoid the terrible user experience of your keyboard shortcuts not working when you happen to be clicked inside of a webview, the default set of webview message handlers have an event called did-keydown. When you load a webview, the following code runs inside the webview to register a handler for it:
contentWindow.addEventListener('keydown', handleInnerKeydown);
/**
* @param {KeyboardEvent} e
*/
const handleInnerKeydown = (e) => {
// ...
hostMessaging.postMessage('did-keydown', {
key: e.key,
keyCode: e.keyCode,
code: e.code,
shiftKey: e.shiftKey,
altKey: e.altKey,
ctrlKey: e.ctrlKey,
metaKey: e.metaKey,
repeat: e.repeat
});
};
How convenient, so webviews just bubble up keydown events so the main VSCode window can treat them seamlessly as user keyboard events.
But…there’s nothing preventing our script running in the untrusted web view from pretending like it’s the user and pressing a bunch of keys on their behalf.
We could, say, bring up the command palette and start running dangerous commands such as installing an attacker-controlled extension. All we’d need is a bit of javascript that emits the correct events to simulate the keystrokes…
developer: install extension from location<attacker controlled extension>In reality it’s not quite that simple. While we can certainly send the keydown events corresponding to that sequence, the browser will not treat it as if it’s the user typing it in. So VSCode will pop up the command palette but unless VScode is intercepting all keydown events to handle each character being typed manually, our events will not actually type text into the palette. Unfortunately, in this case here, it is not listening to keydown events, the command palette widget just uses an HTML <input> tag.
We can scroll up and down in the command palette if we emit up-arrow ↑, down-arrow ↓ presses and can press Enter to select commands but arbitrary keystrokes are off the table.
Luckily, VSCode comes with a massive set of default keyboard shortcuts, all of which listen directly on keydown that we can try to make use of. After a bunch of tinkering, the easiest way I found is to make use of the “Notifications: Accept Notification Primary Action” action. This default keybind of Ctrl+Shift+A will hit the primary button on whatever notification popped up last in VSCode.
Which notification are we accepting?

VSCode has this feature where your workspace can recommend extensions by putting them in a file called .vscode/extensions.json that looks something like
{
"recommendations": [
"HackerMan.my-malicious-extension"
]
}
And then we can use Ctrl+Shift+A to accept that notification and install the malicious extension giving us full code execution? Shrimple as?
Again, not quite, VSCode as of 1.97 has this new publisher trust system whereby installing an extension from a new publisher for the first time gives you this dialog, even if we hit the Install button in that notification:

While we can send Tab key presses to navigate the buttons here, pressing Enter on the Trust Publisher & Install button is impossible as it listens for keydown events specifically on the button and not the entire window.
Instead, we can make use of another VSCode feature called local workspace extensions. As long as you are inside of a trusted workspace (which github.dev/web workspaces always are), then it’s possible to install an extension directly present in .vscode/extensions. Extensions installed in this way skip the trusted publisher check with the trusted workspace check acting as the trust check. So now we can just put our evil payload in .vscode/extensions/extension.js and execute our own code, right?
Well almost, doing this causes a Content Security Policy (CSP) error because the extension worker that loads extensions is expecting them to be from vscode-cdn.net. Local workspace extensions probably weren’t well tested with the web version of VSCode.

This is just a small hiccup though, one of the things that extensions can do as part of their package.json is to contribute extra keybindings to VSCode. Since we can reliably trigger keybindings, we can just add a keybind for whatver VSCode command we want. Such as…installing an extension while skipping the trusted publisher check. So our package.json ends up looking like this to call into workbench.extensions.installExtension while skipping the publisher trust check.
"contributes": {
"keybindings": [
{
"key": "ctrl+f1",
"command": "runCommands",
"args": {
"commands": [
{
"command": "workbench.extensions.installExtension",
"args": [
"AmmarTest.hello-ammar-github",
{
"donotSync": true,
"context": {
"skipPublisherTrust": true
}
}
]
}
]
}
}
]
}
To put it all together, what we need is a repo with a Jupyter notebook and a local workspace extension. The Jupyter notebook needs to execute a little bit of Javascript which we can do with a markdown cell containing the following:
<img src="data:foobar" onerror="javascript(); goes(); here();">
For our Javascript payload, we need to do the following:
keydown event for Ctrl+Shift+A to accept the notification.keydown event for Ctrl+F1 triggering the installation of our chosen extension.This payload ends up looking like:
// Wait for VSCode to load and pop open the notification.
await sleep(10 * 1000);
// ctrl+shift+a, accept the primary notification asking if we want to install
// the recommended extension
window.dispatchEvent(
new KeyboardEvent("keydown", {key: "a", code: "KeyA", keyCode: 65,
ctrlKey: true, shiftKey: true})
);
// Wait a little for the extension to install...
await sleep(500);
// ctrl+f1, the custom keybind to install the chosen extension.
window.dispatchEvent(
new KeyboardEvent("keydown", {key: "F1", code: "F1", keyCode: 112, ctrlKey: true})
);
Now that we’ve seen the details, let’s take a look at the proof-of-concept. For the bravest among you, go ahead and just directly click:
https://github.dev/ammaraskar/github-dev-token-steal-poc/blob/main/README.ipynb
This will launch the github.dev editor directly to the notebook. You’ll see a little status message of what the Javascript payload is doing.

Once the payload runs, the newly installed extension will grab your GitHub API token and then query https://api.github.com/user/repos to get private repos you have access to. It then prints them out and your token in a little information box.

The code for both the repos used is here:
If you run the PoC, remember to either clear your github.dev data (see below) or at the very least uninstall the proof-of-concept extension otherwise it will follow you on all github.dev pages.
This vulnerability also exists in the desktop version of VSCode, though it’s a bit harder to exploit since you would need to convince the victim to clone your repo and open the notebook with the webview script payload. Of course, if you had some other XSS in a webview that you can get a victim to open, you get effectively full RCE on their computer.
By a stroke of luck, if you have never used github.dev in the past, there is one dialog to click through when landing on the website. This didn’t used to happen before but some changing of VSCode’s GitHub plugins has caused this.

This means that if you clear your cookies and local site data for github.dev, you can take action and navigate away from the page if someone tries to use this attack on you. I strongly recommend you clear site data for github.dev, in Chrome this can be done by clicking the little icon in the URL bar, clicking Cookies and site data > Manage on-device site data.

and then deleting data for all the domains with the trash can icons:

Unfortunately, if you’ve ever been past that dialog on github.dev and haven’t cleared your browser’s local storage, you’re completely screwed. There are no CSRF tokens or anything for github.dev so any link on the internet can redirect you to this attack.
VSCode’s approach of not just solely relying on the <iframe> but using defense-in-depth security measures like a strict Content Security Policy and using DOMPurify for rendered markdown pays dividends here. If there was a way to execute arbitrary Javascript inside the Markdown preview shown on an extension’s page, you can imagine how this vulnerability could have even more impact (1-click RCE on desktop by linking someone your extension). However, by using script-src 'none' this is effectively nipped in the bud.

To summarize the last time I interacted with MSRC regarding reporting a VSCode bug, it was a horrible experience where they silently fixed the bug I pointed out without any credit. They also marked it as not having any security impact. As I mentioned in that post, going forward I would be doing full public disclosure for any security bugs I found in VSCode. Taking a look at a recent report by Starlabs on a VSCode XSS bug marked as ineligible and low severity, it doesn’t look like MSRC has gotten any better about VSCode bugs.
I’m sure the VSCode team would have appreciated a longer heads up on this to come up with solutions. There is legitimately a UI/UX balance here that needs to be struck with the security concerns. To those folks, I am sorry, but this is one of the few levers I have to try to influence MSRC and the security posture of VSCode. Finding and fully developing security bugs into proof-of-concepts like this takes time and effort on the part of security researchers that should not be disrespected or taken for granted.