Supply Chain Security

Request Rewriting: How a Browser Extension Forwards Your Session Token Without Reading It

Dark cyberpunk illustration of a night skyway between two towers, where an amber beam crossing the bridge passes through an angled pane of glass that silently splits a cyan copy of it away into the unlit city below.

You approved it. The dialog said the extension could read and change your data on the websites you visit, you wanted the ad blocking, and you clicked Add. That click is the entire authorisation step, and it is the same click on a work laptop signed in to Microsoft 365 as it is on a personal one signed in to a streaming site.

Socket's threat research team published an analysis on 11 September of a cross-store extension called "Twitch Enhanced Viewer | JeetBot", carrying roughly 30,000 users on the Chrome Web Store and 552 on Firefox Add-ons. The extension does what it advertises: it blocks ads, forces 1080p, unlocks regions, and collects channel points. It also forwards the user's live account-scoped Twitch OAuth token to proxy hosts run by a commercial bot service, on every channel the user watches, with a hardcoded exemption list of ten Russian-language streamers whose sessions are left alone. That exemption list is the part that settles intent. Nobody builds a carve-out for a bug.

The Twitch half of the story is a consumer problem. The mechanism is not. The Chrome build does the forwarding with declarativeNetRequest and a regexSubstitution redirect rule, and Chrome's own documentation describes that API as the privacy-preserving one, the one that "lets extensions modify network requests without intercepting them and viewing their content." Both statements are true at the same time. The extension never reads the token. It writes a rule that instructs the browser to append the token to a URL pointed at somebody else's server, and the browser does the rest.

You are in scope if staff sign in to company SaaS in Chrome or Edge on a machine where anyone can install an extension. That is most organisations under a few hundred seats, and the check below takes about ten minutes. You can close this page if your fleet already runs a default-deny ExtensionSettings policy with a reviewed allowlist, or if company SaaS is only reachable from a managed browser profile with extensions off. In that case the JeetBot story is somebody's gaming account and none of this is yours.

The privacy-preserving API is the one that moved the token

Two builds of the same extension show the direction of travel. Version 4.8, from January 2026, did it the obvious way: a POST of the token object to https://enhanced[.]jeetbot[.]cc/set-token, with backup endpoints on deno.dev and deno.net. That is extension code touching a credential and calling out with it, which is exactly the shape a reviewer or an endpoint product has a chance of catching.

The v85.x builds moved the same data through the browser's rule engine instead. Socket documents the current Chrome behaviour as a declarativeNetRequest rule using regexSubstitution to splice &auth=${encodeURIComponent(authToken)} onto a network-layer redirect toward the operator's proxy. The Firefox build uses a blocking webRequest.onBeforeRequest listener to the same end. In the Chrome case the credential ends up in a query string, which means it lands in cleartext in the proxy's access logs, in any intermediate log, and in whatever the operator does with those files afterwards.

The permission model is where defenders should pay attention. Chrome offers two variants of this capability. Declaring declarativeNetRequest triggers a permission warning at install time. Declaring declarativeNetRequestWithHostAccess triggers no warning for the network capability at all - the extension instead requests host permissions, and the user sees the familiar "read and change your data" line for those hosts. A user reading that string is being told the extension can see the pages. They are not being told the browser has been handed a rewrite rule for every request those pages make.

Substitute any host you care about for twitch.tv and the same three lines of manifest give an extension the ability to rewrite requests leaving your Microsoft 365, Google Workspace, Salesforce or internal application tabs. The bearer token in those requests is the same class of object as the one JeetBot forwards: it satisfies the service on its own, after the password and the multi-factor prompt are already behind it.

Find out what is already installed

Most small IT teams have never enumerated this. Extensions install per user profile, they do not appear in Programs and Features, and an asset inventory built from installed software will not list a single one. On Windows the manifests sit under each browser profile and are readable without admin rights, so this runs as the logged-in user or through your RMM.

# Every extension in every Chrome and Edge profile on this host, with the
# hosts and permissions its manifest asked for.
$roots = @{
  Chrome = "$env:LOCALAPPDATA\Google\Chrome\User Data"
  Edge   = "$env:LOCALAPPDATA\Microsoft\Edge\User Data"
}

foreach ($browser in $roots.Keys) {
  Get-ChildItem "$($roots[$browser])\*\Extensions\*\*\manifest.json" -ErrorAction SilentlyContinue |
    ForEach-Object {
      $m = Get-Content $_.FullName -Raw | ConvertFrom-Json
      [pscustomobject]@{
        Browser = $browser
        Id      = $_.Directory.Parent.Name
        Name    = $m.name
        Version = $m.version
        Hosts   = ($m.host_permissions -join ' ')
        Perms   = ($m.permissions      -join ' ')
      }
    }
} | Sort-Object Browser, Id | Format-Table -AutoSize

Read the output on Hosts and Perms first and the name last. A Name value like __MSG_extName__ is normal - it means the display name is localised and lives in the _locales folder, so look it up by ID rather than assuming the manifest is hiding something. The IDs worth pulling out of the list are the ones whose host_permissions include <all_urls>, *://*/*, or any host you authenticate against, combined with declarativeNetRequest, declarativeNetRequestWithHostAccess, webRequest, webRequestBlocking, proxy, cookies or debugger. That combination is the token-forwarding primitive. It is also, unhelpfully, what several legitimate ad blockers and password managers declare, which is why the answer is an allowlist rather than a blocklist.

Default-deny is one policy value

Chrome and Edge both take a single JSON policy called ExtensionSettings (Chromium schema, Edge reference) that overrides every other extension policy. Set the "*" default to blocked and name the handful you have reviewed. Nothing else installs, and anything already installed that you mark removed is uninstalled on the next policy refresh rather than merely disabled.

{
  "*": {
    "installation_mode": "blocked",
    "blocked_install_message": "Ask IT to review this extension before installing it.",
    "blocked_permissions": ["proxy", "debugger", "webRequestBlocking"]
  },
  "pnhhdhhcadcjfckjhpmjneldiegbojfb": {
    "installation_mode": "removed"
  },
  "cjpalhdlnbpafiamejdnhcphjbkeiagm": {
    "installation_mode": "allowed"
  },
  "update_url:https://clients2.google.com/service/update2/crx": {
    "installation_mode": "blocked"
  }
}

The delivery path depends on what you already run. Chrome reads it from HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Google\Chrome\ExtensionSettings; Edge reads it as a REG_SZ value named ExtensionSettings under SOFTWARE\Policies\Microsoft\Edge. Both are reachable from Group Policy with the vendor ADMX templates, from an Intune settings catalog or custom OMA-URI profile, and from the Google Admin console for Chrome. There is no licence to buy for any of those paths, which is the reason this control is worth more to a forty-person company than most of what it is currently paying for.

Three details that decide whether the policy holds

  • The last entry blocks a whole store. Keying on update_url: for the Chrome Web Store stops Edge users sideloading from Google's catalogue while your allowlist still force-installs the ones you approved. Per-ID entries win over it.
  • removed uninstalls, blocked only prevents. If an extension is already deployed across the fleet, blocked leaves it running on every machine that has it. Use removed for anything you are responding to.
  • Personal profiles slip the policy. Machine-level policy applies to the browser, but a user signed into a personal Chrome profile syncs their own extension set. Pair the policy with a profile restriction, or accept that the control ends where the managed profile does.

An extension incident is a credential incident

The JeetBot developer shipped 85.8.7 to the Firefox listing with the forwarding removed, and warned users that the update does nothing about tokens already sent. That warning generalises to every case of this kind. Removing the extension stops the next token from leaving. It has no effect on the ones already sitting in somebody's logs, and those keep working until the issuing service is told otherwise.

So the response order is revoke, then remove, then review. Socket's guidance for the Twitch case is to disconnect all sessions in account settings and re-authenticate. For a work identity the equivalent is a tenant-side revocation, which invalidates refresh tokens and browser session cookies by stamping signInSessionsValidFromDateTime forward.

# Invalidate every refresh token and session cookie for one user.
# Least-privileged Graph permission: User.RevokeSessions.All
Connect-MgGraph -Scopes "User.RevokeSessions.All"
Revoke-MgUserSignInSession -UserId user@example.com

# Same operation over raw Graph, for scripting across a set of users.
POST https://graph.microsoft.com/v1.0/users/{id}/revokeSignInSessions

Microsoft notes a delay of a few minutes before revocation takes effect, and the call does not touch guest users, who sign in through their home tenant. Do the same at every other service the extension had host access to. Google Workspace sign-out is under the admin console user record, Salesforce sessions are ended from Setup, and anything with a personal access token needs that token rotated rather than the session dropped.

One judgement call I make on Red Hound engagements and would make again here: do not spend the first hour of this trying to prove the extension was malicious. The install counts on these listings are public, the manifest is readable, and the permission set either includes the rewrite primitive or it does not. If it does and nobody can say why the business needs that extension, revoke the sessions and pull it. Proving intent is a research exercise that can happen after the tokens are dead.

Push the extension policy first, then read the inventory

The order matters more than the speed. Deploy the default-deny ExtensionSettings value in report mode against one pilot group, confirm that the four or five things people genuinely need are on the allowlist, then enforce it fleet-wide. Only then run the inventory script, because until the policy is live your list is a snapshot that changes every time somebody installs something. An inventory without a policy is a report. A policy without an inventory still blocks the next JeetBot.

If you want a second set of eyes on the allowlist, or on which of your SaaS sessions would actually be reachable from a browser extension, that is the kind of scoped review we do. We assess Microsoft 365 and Entra ID environments for the configuration gaps attackers use, and browser policy has moved firmly into that category.

Need help hardening your identity infrastructure?

We assess Microsoft 365 and Entra ID environments for the misconfigurations attackers actually exploit, browser and session policy included. Book a session to discuss your environment.