5
 min read
June 10, 2025
|
Updated: 

Chromium's security blind spot: The case of the hidden extension

A Chromium security blind spot lets attackers take over the browser while maintaining persistence and leaving no noticeable traces. We addressed the issue at Island, but further exploration is needed.

If you're reading this, chances are you spend much of your day in front of a computer, running all kinds of programs and scripts from the internet. But have you ever asked yourself how safe those scripts are? Or whether there's even a way to be sure they're free of malicious code?

In this blog post, we discuss the dangers of local attacks and demonstrate how Chromium-based browsers can be abused to run arbitrary code with extensive capabilities, while maintaining persistence and leaving no noticeable traces. This can be achieved through a small modification to the Chromium profile files, without requiring administrator privileges.

Can you really trust your computer?

Even if you’re very careful, it’s nearly impossible to be sure that during a prolonged period of time, no malicious code was running. I know I can’t say that for sure about my computer. Given such a possibility, how can you make sure that your computer is not currently compromised, without extreme measures such as formatting the computer and starting from scratch?

One can argue that sophisticated malware can be very difficult to detect, so let’s make some assumptions to narrow the possibilities:

  • No 0-day or 1-day exploits, no nation-state attacks. You’re probably not that important, and hopefully you keep your software updated, so 1-day exploitation is unlikely.
  • The malicious code runs with no administrator privileges. In standard desktop operating systems, the default user account operates under restricted privileges. For example, a program or a script can’t modify system files or system configuration by default.

Even with these assumptions, malicious code can gain powerful capabilities, such as logging all keystrokes, taking screenshots, accessing personal files, and accessing the webcam and microphone (although some OSes make it more difficult). To be able to use these capabilities even after system reboot, the malicious code has to take care of persistence, i.e. to register itself somehow to be triggered the next time the OS starts or when some event occurs.

At this point, one can suggest the following:

  • Verify that executables and modules aren’t tampered with. Major components, such as OS modules and browser executables, are digitally signed and can be verified.
  • Scrutinize programs’ external sources. Once verified that executables are malware-free, malicious code can only come from external sources such as command-line flags, textual scripts, or data files.

Some programs, such as PowerShell, bash, or Python, are designed to run arbitrary code which is not part of the executable. Such programs are often misused, and are sometimes blocked in hardened environments. Also, antivirus solutions are known to treat them with suspicion. For the purpose of this discussion, let us assume that these types of programs are blocked.

What are we left with? Core OS components are unlikely to run code from external sources, but there’s a program which you’re using right now which does that, and which you won’t want to block: your browser! Nearly every modern browser provides a way to install extensions, which can have extensive capabilities, such as accessing all browsing data, logging keystrokes and even accessing the webcam and microphone. Luckily, it’s not a big deal. It’s enough to just verify that no suspicious browser extensions are installed. Or is it?

Invisible extensions in Chromium

About three years ago, we wrote about extension locations in Chromium. In short, for each installed extension, Chromium keeps track of its source location type, which affects the way the extension is treated. For example:

  • kInternal: Extensions explicitly installed by the user.
  • kExternalRegistry: Extensions that are installed via the Windows registry.
  • kCommandLine: Extensions loaded via the --load-extension command line flag.
  • kComponent: Integral components of the browser itself, which happen to be implemented as extensions (e.g. PDF Viewer). Component extensions are not displayed on the extensions page (chrome://extensions) and have some extra privileges.

The special treatment for component extensions made us wonder whether it’s possible to take a regular extension and manipulate Chromium to treat it as a component extension, hiding it from plain sight. We tried that by simply changing an extension’s location value in the preferences file that’s stored in the Chromium profile folder. After changing a regular extension’s location value to be a component extension and launching the browser, the extension was uninstalled right away. Well, we didn’t expect it to work that easily, but let’s take a look at the code that uninstalls the extension in this case:

void ExtensionService::CheckExternalUninstall(const std::string& id) {
  CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));

  // Check if the providers know about this extension.
  for (const auto& provider : external_extension_providers_) {
    DCHECK(provider->IsReady());
    if (provider->HasExtension(id))
      return;  // Yup, known extension, don't uninstall.
  }

  // ...snip...
  UninstallExtension(id, UNINSTALL_REASON_ORPHANED_EXTERNAL_EXTENSION, nullptr);
}

The CheckExternalUninstall function uninstalls an extension if no provider knows about it. The component extension provider has a very short list of hardcoded extensions it knows about, and our extension is obviously not among them. But do you see the problem with this check? It’s enough for any provider to know about an extension to keep it installed. Simply adding the extension to the preferences JSON or registry adds it to the relevant provider, and keeps it from being uninstalled.

The result: An ability to hide any extension by modifying the preferences file in the Chromium profile folder, and adding a registry entry (Windows) or a preferences file in the user’s profile folder (macOS/Linux). Neither operations require administrator privileges.

But hiding an arbitrary extension, such as an ad blocker, won’t help an attacker do much harm. There are several options here. We decided to explore what it takes to modify an existing extension that’s installed from the Chrome Web Store.

Extension verification, how secure is it?

For extensions which are installed from the Chrome Web Store, Chromium normally performs content verification when extension files are accessed. Once a tampered file is detected, the extension is disabled and marked as corrupted. The verification is done using asymmetric encryption, making it infeasible to just recalculate the correct signature of a modified file. Does that mean that there’s no way to add custom code to such an extension? As it turns out, not at all. A shallow look at the code was enough to find several ways to do that without invalidating the signature.

One interesting function that plays a significant role in the content verification process is GetVerifiedFileType. For each accessed extension file, it decides whether the file needs to be verified. Some files are being created or modified during the extension installation process, making their signature invalid or non-existent, so they must not be verified. This means that if we manage to place our code in one of these files and get it to run, we can take over an extension without affecting its verified status.
Here’s a simplified pseudo-code version of the GetVerifiedFileType function:

1. def should_verify(path): 
2.   if path == manifest_path: 
3.     return False 
4. 
5.   # Check if bg script, bg page, service worker script, or content script.
6.   if path in sensitive_paths: 
7.     return True
8. 
9.   if path.ext in ['.js', '.htm', '.html']:
10.     return True
11.
12.   if path in img_paths:
13.     return False
14.
15.   # Other checks...

Misusing manifest.json

The manifest.json file is the only file that every extension must contain. It specifies basic metadata about the extension such as its name and version, and core aspects of the extension’s functionality such as background scripts, content scripts, and browser actions. When an extension is installed, Chromium copies the metadata to another location, and this file is no longer used.

Roi Leibovich and I discovered that the first thing the GetVerifiedFileType function does is check whether the accessed file is manifest.json:

 2.   if path == manifest_path:
 3.     return False

If it is, no verification is performed. But since it’s no longer used by the browser, we can override it with any other content and use the file as a script file. Surprisingly, it just works! We were able to use this bypass to add custom content script code and background script code (for MV2 extensions, now deprecated). But newer extensions (MV3) replace background scripts with service workers, and they fail to be loaded from .json files. So we went on looking for other bypasses.

Path manipulations

We want to have a JavaScript file to run as a service worker, and still bypass the following two conditions:

  • Is it a sensitive part of the extension: background script, background page, service worker script, or content script? If it is, verify it. (lines 5-7)
  • Is it a JavaScript file or an html file? If it is, verify it. (lines 9-10)
 5.   # Check if bg script, bg page, service worker script, or content script.
 6.   if path in sensitive_paths:
 7.     return True
 8.
 9.   if path.ext in ['.js', '.htm', '.html']:
10.     return True

Here’s how we did that:

  • Normally, the requested file path will match the service worker path, causing it to be verified. But due to inconsistencies in path handling between the request part and the file verification part, starting the path with a parent folder works, i.e. “../script.js” instead of “script.js”. The request part ignores the leading parent folder, while the verification part rejects the whole path. Another bypass that works on Windows is adding a trailing period or space to the file name, which gets removed in the verification part, but not in the request part, causing a mismatch.
  • The second check considers a file to be a JavaScript file if its extension is .js, but there’s at least one additional extension which is considered to be a JavaScript file and can be loaded as a service worker: .mjs.

Image entries

The next condition checks whether the path is registered as an image file. If it is, the verification isn’t performed. (lines 12-13)

12.   if path in img_paths:
13.     return False

For matching the condition and ensuring that no verification is performed, it’s enough to register the new service worker file as an extension icon. It doesn’t have to be a valid image or have an image extension.

Demonstration

We created a simple proof of concept Python script that modifies a Chromium profile to achieve the following:

  • Install an extension.
  • Hide the newly added extension from the UI.
  • Add custom code to the extension without violating its integrity and triggering a reinstallation.
  • Make some permission adjustments such as allowing webcam access.

The script can be found in our GitHub repository:

https://github.com/island-io/chromium-profile-tampering-poc

Figure 1 After running the proof of concept script: no extensions in sight, but a hidden extension demonstrates access to cookies, the filesystem, and the webcam.

The script supports Windows and macOS, and any Chromium-based browser that lacks additional hardening, including Google Chrome and Microsoft Edge. The script doesn’t require administrator privileges. Note that due to some of the improvements that we submitted to the Chromium project, the proof of concept only works with Chromium versions older than 136. The full list of submissions can be found at the bottom of the post.

After running the script, the browser will look exactly the same - there won’t be an additional extension in the extensions page or in the toolbar to the right of the address bar. The only obscure places where the change can be seen are:

  • The chrome://system page.
  • The browser’s task manager, only if a background script or service worker is running. The extension can have any name, we called it “Chromium Crash Reporter” in our proof of concept.
  • In the extensions page when the browser is launched with the --show-component-extension-options command line flag.
  • In the raw configuration files in the profile folder.

An average technical user is likely unaware of these, let alone a casual user.

Demonstrated capabilities

As noted previously, a browser extension can gain lots of capabilities. A good review on the topic can be found in the following blog post: Let's build a Chrome extension that steals everything. Instead of demonstrating each and every possible capability, we picked the following capabilities which we didn’t see mentioned in other places, and which might surprise you:

  • Webcam access without asking for permission and without visual indicators. Normally, a website or an extension must get the user’s permission to access the webcam. After permission is granted, the requesting tab shows a recording indicator that’s hard to miss. We granted permission to our extension by modifying the preferences file, and we were able to avoid the recording indicator by doing the recording in an offscreen page, a questionable capability that’s allowed by the browser. Of course, the same technique can be applied for other operations, such as accessing the microphone, the clipboard, etc.
  • Unlimited read-only access to all files on the system. Extensions have an option to allow access to file URLs, which is disabled by default but can be enabled in the extensions page. Since we’re editing the profile files directly, we can enable this option for our extension, granting it access to all files - desktop files, family photos, personal documents, everything. The proof of concept demonstrates the ability to list the files in C:\ and to read the C:\password.txt file content.

Is it a security vulnerability?

This is not considered a security vulnerability according to Chromium’s threat model. From the Chrome Security FAQ:

“People sometimes report that they can compromise Chrome by [...] altering the configuration of the device.

We consider these attacks outside Chrome's threat model, because there is no way for Chrome (or any application) to defend against a malicious user who has managed to log into your device as you, or who can run software with the privileges of your operating system user account. [...] Such an attacker has total control over your device, and nothing Chrome can do would provide a serious guarantee of defense.”

While it’s true that Chrome can’t provide guarantees when running in a compromised environment with malicious code running in the background, the situation isn’t quite the same when it comes to tampering with profile files. In this case, the system isn’t compromised, no malicious code is running, and the only things that are changed are the browser’s profile data files.

Though it’s challenging to provide guarantees in this case, it’s also possible to implement additional safeguards. For instance, it is possible to ensure that third-party extensions can’t be hidden or tampered with. In fact, Chrome already makes efforts in that direction, and while the current implementation can be bypassed, it’s understandable that achieving perfect defenses is difficult in such a complex ecosystem. Our contributions aim to help make the Chromium project more secure, which benefits all Chromium-based browsers. As one of the most widely used software platforms in the world, it seems prudent to consider and mitigate such attack vectors where feasible.

In this blog post, we only looked at a few possible misuses for profile tampering. There are many other issues which can be used to violate Chrome’s integrity, which don’t meet the bar for Chrome’s security standards, but can nevertheless have real impact on users’ security.

We discussed Chromium’s threat model and its implications in depth in our recent blog post The Chromium Security Paradox.

Our contributions and open issues

It was important for us to make sure that the information we share can’t be easily misused, regardless of whether it’s officially considered as a security vulnerability. We submitted several improvements to the Chromium project which make our proof of concept script, and this specific attack in general, no longer relevant. Despite that, we only looked at a small part of the code, and it won’t surprise us if more gaps are found throughout the code.

Contributions

  • Verify extension location in CheckExternalUninstall
    The additional verification makes it impossible to register an extension in another external provider to prevent it from being uninstalled. This makes our method of hiding extensions by marking them as component extensions unusable.
  • Handle paths with . or .. components
    The extension's manifest paths, including relative ones, are now normalized to ensure consistency. For example, paths like src/service_worker.js and ../src//./a/../service_worker.js are correctly treated as equivalent.

Open issues

  • Lack of signature for manifest field
    Chromium ensures that extensions installed from the Chrome Web Store can't be tampered with, but it does not validate the manifest fields, which can still be modified after installation.

Where do we go from here?

The Chromium browser project, while equipped with advanced security technologies to defend against remote threats, remains vulnerable to local access attacks. This limitation stems from Chromium’s threat model, which is primarily designed to protect against malicious web content rather than adversaries with local access to a compromised system. As a result, vulnerabilities like the ones we demonstrated are often considered “out of scope.” While Chromium is a strong choice for general internet use, enterprise environments that manage sensitive data require a broader security approach—one that includes protection against both remote and local threats.

To address these challenges, the Island Enterprise Browser extends Chromium with enterprise-grade security and management capabilities. It incorporates advanced integrity verification checks to detect tampering and ensure the trustworthiness of the browser environment—critical for defending against local attacks. Island also provides fine-grained control over user interactions (e.g., copy/paste, downloads, screen capture), integrated device posture checks, dynamic policy enforcement, and forensic audit logging. These enhancements allow enterprises to secure sensitive web applications, support BYOD usage, and implement Zero Trust principles, creating a more resilient and controlled browsing environment.

Michael Maltsev

Michael is a Tech Lead at Island. As part of the core team, he is responsible for developing the Island browser as well as leading research in the Chromium and security fields. Before Island, Michael spent years as a vulnerability researcher, focusing on Windows security and exploitation.

No items found.