Attacking browser extensions

Learn about browser extension security and secure your extensions with the help of CodeQL.

| 19 minutes

Browser extensions first became mainstream in the early 2000s with their adoption by Firefox and Chromium and their popularity has been growing ever since. Nowadays, it is common for even the average user to have at least one extension installed, often an adblocker. Research into the security of browser extensions is mostly scattered around between individual bug reports and coverage on malicious chrome extensions. In this blog, I will introduce the structure of a browser extension and the vulnerabilities that are present in the ecosystem. I will then discuss the progression of security in the extension space, highlighting the attack surface and its relationship with mitigations that have been implemented. Lastly, I will recommend some CodeQL queries and best practices that users, developers and researchers can use to ensure the security of their extension.

The extension structure

Mozilla and Google, and their respective browsers, Firefox and Chromium, set the standard for most browser extensions (note, we will not cover Apple’s Safari here). Throughout this blog, I will talk about extension core concepts, and highlight the differences between Firefox and Chromium. The differences between Firefox and Chromium are manifested in the differences in policy on what is allowed on the corresponding extension stores and how extensions interact with the browser, which ultimately decide the security and safety of extension for the end user.

A browser extension is a group of HTML, CSS, and JavaScript files that work together to enhance the browsing experience. Usually, the code runs in its own domain, the domain labeled by the extensions ID. For example, the Chromium extension uBlock origin https://chromewebstore.google.com/detail/ublock-origin/cjpalhdlnbpafiamejdnhcphjbkeiagm will run in the domain cjpalhdlnbpafiamejdnhcphjbkeiagm, which the Chromium web store makes obvious via the URL. Extension URLs vary depending on the browser, but generally follow the pattern: browser_specific_extension_scheme://extension_id/actual_resource_name. If you want to access the popup present on uBlock origin Chromium extension, you can use the URL: chrome-extension://cjpalhdlnbpafiamejdnhcphjbkeiagm/popup-fenix.html.

Besides the HTML, CSS and JavaScript files, an extension also has one important settings file, named manifest.json. This is a required file that lists the identification of the extension, permissions required of the extension, and the accessibility of the extension. As the ecosystem of browsers have progressed, the version of the manifest.json has also progressed, with new versions often enforcing more secure settings and making changes to the nomenclature. In the next section, we will discuss the contexts an extension’s files can run in and how this can be directed via the manifest file.

In the manifest.json file, we can specify the context that a file will run. The three major contexts are the webpage/content script, the popup and the background. You can let the browser know which context you want the file to run in by specifying it in the manifest.json file, aptly labeled content_scripts, background_script, and browser_action in the manifest version 2 (v2). On manifest version 3 (v3), a manifest.json may look like the following:

Screenshot of a manifest.json file showing minor changes between versions.

Here, you can see how the v2 permissions have changed slightly to their v3 counterparts: content_scripts, background and action, showing the minor changes between versions.

Background script and permissions

Let’s start by talking about the background context. The background context is the most powerful of the three contexts with the ability to access most of the browser extension APIs/WebExtensions API. From now on, I will refer to both browser extension APIs and WebExtensions API as the Extension APIs for the sake of brevity. The Extension APIs give an extension a lot of control of the user’s browsing experience, with the ability to arbitrarily control tabs, read from the websites, or modify and read cookies, to name a few. Luckily, these abilities are each locked behind permissions, requested in the manifest.json file under the “permissions” key. When installing an extension, a popup will show up describing in a user friendly way which permissions an extension will be granted.

Screenshot of the popup describing which permissions the uBlock Origin extension will be granted.

There are some important permissions to look out for during a security review of an extension. Firstly, look for the permissions key in manifest.json, whose values are a combination of hosts and actual permissions in manifest version 2.

Screenshot of the permissions key in manifest.json

In this example, the extension can access all URLs, which can be specified through regex or through the keyword all_urls. When an extension has permissions for a domain, it will allow the extension to send requests to the domain with all cookies, ignoring some security considerations. For example, if the website puts some SameSite strict cookies in the browser, the extension can still make requests on your behalf with those cookies despite being part of a separate domain. In v3, domain permissions are moved to host_permissions and additional optional permissions are introduced, which are requested during runtime based on user consent. A v3 manifest may look like:

Screenshot of a v3 manifest

Some important permissions to look for are those that include user sensitive information, such as history, bookmarks, cookies or permissions or ones that give the extension more control over the browser, such as downloads, management, or tab. One permission that has a lot of power is the activeTab permission, allowing the extension to inject JavaScript code into any domain that the user is currently interacting with. In order to inject into the current tab, it must have user interaction. This activeTab is interesting for exploitation and malicious extensions alike due to its immense power. If malicious input can be injected into the executed JavaScript, the attacker may get the ability to get Universal XSS (UXSS). A malicious extension, on the other hand, can create shortcuts that overlap with common user actions, such as copy or paste, and interact with tabs it is not supposed to have access to. The permissions of an extension are a great way to start assessing an extension to see if it even has enough privileges to perform actions that may pose a risk. The background script should be audited to ensure the safety of calls to the browser APIs, and analyzed to see how messages are sent back to the content scripts.

Content scripts

The frontend of an extension is just as important as the backend when extensions want to interact with the DOM of the pages visited by the user, a responsibility that a background script cannot achieve due to its lack of access to the DOM. This is where content scripts come into play. The content script runs in the context of the website but lives in an isolated world, where “JavaScript variables in an extension’s content scripts are not visible to the host page or other extensions’ content scripts.” For example, if an extension wanted to add the summary of a page to the top to help readability, the code may look something like this.

Sample code adding a summary of a page to the top to help readability

The content script may also listen for user interaction on the current page, allowing an action to be taken based on the user interaction. For example, some extensions translate text present on the current page, based on the user’s highlight or focus on a certain word or phrase. The content script and background script work in harmony in order to create the extension’s intended experience.

Lastly, the popup context is present for the HTML and JavaScript that makes up the menu that “pops up” when you click on the icon of the extension. Often, the popup will let the user directly interact with the functionality of the extension, usually allowing them to change settings and make requests to backend servers that are tied to the extension.

Screenshot of a uBlock Origin popup for a Google Docs page, showing what has been blocked and allowing the user to interact directly with the extension.

For example, in the uBlock origin popup, we can click on different icons to access the options page, disable fonts and JavaScript, or disable the extension on the current website.

The popup page, along with the other HTML pages included in the extension, can be a critical source of interest. For example, MetaMask is a crypto wallet extension. If a website can cover the extension, a malicious website can trick the user into signing transactions and thus result in the loss of funds.

Screenshot of two popups from the the crypto wallet extension MetaMask.

Like the background script, the JavaScript running in the popup page can use all the Extensions APIs that the extension has permissions for and any JavaScript runs in the domain of the extension.

Attack surface

Because browser extensions are made from HTML, CSS, and JavaScript, they are vulnerable to many of the classic JavaScript vulnerabilities. I will first introduce the attack surface of v2 extensions because it is a superset of v3, then I will conclude by talking about the mitigations brought about in v3 and how it restricts the attacker. I will also write about browser specific implementations and how they affect security.

All attacks must start from an attacker-controlled source, and the extension interacts with two main attacker-controlled sources:

  1. A website loaded by the user
  2. Other installed extensions

The most common attack surface in the content script occurs when data is parsed from the current website and the script injects the data into the document as HTML. Some extensions extract the DOM text and attempt to make changes, often to beautify the text or to use the data in some way, then return the DOM text back to the webpage. If the extension allows the text to be inserted back into the page as HTML, we can get an XSS vulnerability in the website. However, this has the prerequisite that the extracted DOM text is controlled by an attacker. Common cases may include when a user comments on a website, or on websites that allow user uploaded content like many social media sites.

Secondly, an extension can interact with another extension by calling the sendMessage API to send a message and onConnectExternal/onMessageExternal APIs to receive a message. If the extension does not check the sender, a malicious extension may be able to access any functionality that the onMessageExternal/onConnectExternal function facilitates.

Depending on the configuration of the manifest, new vulnerabilities can be introduced. Let’s see some of those.

external_connectable property

The external_connectable property allows an extension to be connected to by a given website or extension ID. Here we can see how onMessageExternal/onConnectExternal can be extremely dangerous if there is a misconfiguration, as the functionality that was only meant for other extensions is now available to websites.

Another interesting configuration property is the web_accessible_resources:

web_accessible_resources code

This property opens up an extension up to a greater attack surface by introducing two new possible attack vectors. If an HTML file is web accessible, then a website can load the HTML file in an iframe. If the page takes URL parameters and uses them in any privileged way, a malicious website may be able to make privileged actions. Secondly, if the HTML page allows for sensitive actions and is web accessible a clickjacking vulnerability is possible, where the website will cover the iframe and get the user to input or click on privileged operations. More information on clickjacking can be found on this great blog post showing an attack on Privacy Badger.

Thus, our three attack surfaces boil down to:

  1. The extension takes attacker-supplied input from the website and uses it in some unsafe way.
  2. Another extension or a website sends a message to the extension and the extension uses that input in a dangerous way.
  3. The extension takes in URL parameters when it is loaded, and those parameters are used to do a privileged operation. This requires a vulnerable configuration.

A quick assessment of all these vulnerabilities shows us why browser extensions are generally pretty secure, because it often requires multiple points of failure in order to introduce an exploitable vulnerability. Often, a misconfiguration is needed alongside a vulnerability in order to make the vulnerability truly exploitable.

Next, we will take a look at the possible vulnerabilities that occur in a browser extension, and the mitigations that browser developers have developed to mitigate these issues.

Vulnerabilities

Cross-site scripting

Cross-site scripting vulnerabilities are present across many web applications, and are present in browser extensions as well. XSS can occur in two contexts, in the context of the content script and in the context of the background script. An XSS gives the attacker the same privileges as the running JavaScript, therefore an XSS in the context of the content script allows the attacker to compromise the user on that specific website. In contrast, an XSS in the context of the background script allows the attacker to call any Extension API the extension has permissions for, and thus gives the attacker much more control over the entire browser (for example, UXSS).

In order to talk about XSS, we must talk about the Content Security Policy (CSP). On manifest v2 and v3 of extensions, the unsafe-inline directive is not allowed in the extension. Therefore, any HTML pages that are part of the extension such as the popup, the options page or any other, are immune from XSS. They are, however, still vulnerable to HTML injection attacks. However, the unsafe-eval attribute is still allowed on manifest v2 but has since been deprecated in manifest v3. When looking for XSS vulnerabilities in extensions, check if the manifest contains the unsafe-eval directive.

When looking for XSS vulnerabilities in extensions, check if the manifest contains the unsafe-eval directive.

Then, look for functions that execute code such as eval(), Function(), setTimeout(), setInterval(), etc. Another function to look out for is the Extension API function executeScript. On manifest v2, the API is tabs.executeScript() and allows taking in a string as code, so it is just like the eval() function. Manifest v3 has removed this API, introducing a new API called scripting.executeScript() which only allows local files to be executed. Generally speaking, outdated Firefox extensions are much more likely to be vulnerable to XSS, as Firefox AddOn Store is still accepting new manifest v2 extensions and thus has access to unsafe-eval directive and the tabs.executeScript() API. Many actively developed extensions even have a Firefox v2 and Chromium v3 extension, due to Google’s push for the new version. I want to shout out this article by Wladimir Palant, which goes into detail about the possible vulnerabilities that occur when using old versions of jQuery and unsafe-eval, and shows some code demonstrating the attacks.

SSRF

Server-Side request forgery is a common vulnerability found in web applications, and we can also find them in browser extensions. Browser extensions are similar to web applications, but run with the cookies of a client-side browser. If an attacker is able to influence the URL of a network request such as XMLHTTPRequest or fetch, it is possible that an SSRF can result. The effect of the SSRF will depend on the specified method, whether the extension developer makes the request with credentials/cookies, and whether the manifest of the extension has that website allowed in the permissions/host_permissions entry.

In its move from v2 to v3, changes to an extension’s ability to make requests have been implemented. Specifically on manifest v2 and on Firefox, an extension with no permissions is able to send requests to arbitrary domains with cookies, but is not able to send SameSite cookies, which is reserved for those with the correct permissions. In Chromium, an extension with no permissions cannot send requests with cookies. In contrast, v3 extensions require the host_permissions in order to send any cookies with the request. This means that SSRF with v2 in Firefox is much more powerful than with v3, depending on the security of the website you are targeting. This, along with Firefox’s lack of enforcement for new extensions to be v3 in the addons store, makes Firefox extensions more vulnerable to SSRF attacks than Chromium.

Extension API injection

Injection into the Extension APIs is a vulnerability unique to browser extensions. If attacker-controlled data is able to be injected into an API call, an attacker may gain the abilities of the extension. Generally, the APIs fall into two categories, those that change data and those that leak data. Some of the APIs that allow you to change data include downloads.download(), the bookmark create or remove function, and even the cookies set function. Unsurprisingly, these APIs are made with safe defaults present. The download method can only download to the user-defined Downloads folder, and is sanitized from path traversal and the like.

Another example would be tabs.update(). If you go into your browser’s bar and type in javascript:alert(document.domain)while accessing a website, you should get an alert showing the website’s domain. A XSS used to be possible using tabs.update() by doing this exact action programmatically, which has since been patched. Most API Injection attacks lead to DOS attacks or information loss, where an attacker can download an infinite number of files until the disk is full, or create/remove bookmarks to annoy the user, but are not as powerful as the traditional web application primitives.

Mitigations

I have discussed many mitigations that have come from the transition from v2 to v3 in the respective vulnerability categories above, but I would like to highlight one browser-specific mitigation below.

UUID randomization

Now that we understand the attack surface and some common vulnerabilities, we can see what mitigations browser developers have put in place to prevent vulnerabilities. Firstly, many browsers have randomized the ID of the extension, so that attacking exposed HTML files is no longer a threat, unless a leak of the internal ID, called the UUID, can be found.

Screenshot of developer details for the extension FoxyProxy.

Here, we can see that all files, such as the manifest, are relative to the Internal UUID. Content scripts can still access the pages by calling browser.runtime.getURL, but this is not possible from within the page itself due to this change. From my testing, both Firefox and Safari randomize this UUID by default, while Chromium does not (at the time of writing this blog). Firefox randomizes based on the container and Safari on application restart. Despite claims both in Chromium and MDN docs that the Chromium browser will randomize the UUID if given the key use_dynamic_url in the manifest being in the docs for many years, this feature was only implemented and enabled by default in August 2024.

Modeling with CodeQL

Just like many web application vulnerabilities, vulnerabilities in browser extensions can be modeled in CodeQL. CodeQL already has support for many of the vulnerabilities, such as XSS and SSRF, which we can use as the base for our queries. CodeQL’s code injection query gets a RemoteFlowSource (any Javascript APIs that potentially take in data from an external system or user) and looks for flow into Javascript code injection sinks such as eval. These sinks are relevant for browser extensions, but more sinks applicable only to browsers exist. We can create a module of sinks that would apply to browser extensions and extend the Code Injection query’s Sink class to tell CodeQL to consider these sinks.

/**
* Sink for chrome.tabs.executeScript() which may allow an allow arbitrary   * javascript execution.
**/
class ExecuteScript extends DataFlow::Node {
  ExecuteScript() { exists( DataFlow::CallNode c | 
    c = tabsRef().getAMethodCall("executeScript") | (this = c.getArgument(0) and c.getNumArgument() = 1) 
    or
    (this = c.getArgument(1) and c.getNumArgument() = 2 ) )}
}

Here, we get a dataflow node that corresponds to browser.tabs and look for the executeScript method. If the method is called with one argument, then it looks for the only argument, but if it has two arguments then the method looks for the second argument, because the first argument is the tabID.

Screenshot of the execute script method

When CodeQL is trying to find vulnerabilities in source code, it needs to know how data flows if an external API is called whose implementation is not given. The Chrome APIs sources are not included in a browser extension, therefore we need to model how the foreground script communicates with the background script.

class BrowserStep extends DataFlow::SharedFlowStep {
    override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
      (exists (DataFlow::ParameterNode p |
        pred instanceof BrowserAPI::SendMessage and
        succ = p and 
           p.getParameter() instanceof BrowserAPI::AddListener
      ))
    }
  }

  class ReturnStep extends DataFlow::SharedFlowStep {
    override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
      (exists (DataFlow::ParameterNode p |
        succ instanceof BrowserAPI::SendMessageReturnValue and
        pred = p.getAnInvocation().getArgument(0) and 
           p.getParameter() instanceof BrowserAPI::AddListenerReturn
      ))
    }
  }

In Javascript CodeQL, we can extend the SharedFlowStep class in order to tell CodeQL that data flows between two data flow nodes. In this first class, I tell CodeQL that data travels between parameter one of sendMessage in the foreground script to the third parameter of the AddListener method in the background script, all in one step. Likewise, the second class models the background script sending a message to the foreground script. With these models in place, we are able to use the Code Injection and CSRF queries to help find XSS and SSRF in browser extensions. CodeQL packs with support for browser extensions are available at our Community Pack repository for developers and researchers to use.

Real-world attack

In this example, I will show how these CodeQL models found a Universal XSS (UXSS) vulnerability in smartup, an extension that has over 100,000 downloads.

Smartup is an extension that allows users to do an action in the browser after a gesture has been taken by the user. The extensions takes untrusted input via onMessageExternal, does a variety of parsing on the message, and eventually processes the message. In one case, apps_test, the extensions uses the chrome.tabs.executeScript v2 API and appends the message property apptype to the code, resulting in XSS. Due to smartup’s broad permission policy (access to all urls or activeTab) and its permissive message receiving policy (arbitrary browser extensions can send it messages), an extension downloaded by the user with no permissions can get UXSS on any website.

chrome.runtime.onMessageExternal.addListener(function(message,sender,sendResponse){
    sub.funOnMessage(message,sender,sendResponse);
})
...
case"apps_test":
    let _fun=function(){
        if(message.appjs){
            chrome.tabs.executeScript({code:"sue.apps['"+message.apptype+"'].initUI();",runAt:"document_start"});      <----- message is passed into executeScript
            return;
        }

This vulnerability may seem serious, but it required three points of failure by the developer (broad permissions, open messaging policy and code injection vulnerability) in order to be fully exploitable. Efforts to inform developers about the importance of security and the risks they take when changing secure defaults should help reduce similar security issues.

Conclusion

Now that you know the security model of browser extensions, what can you do as a user to ensure that your extensions are secure? First, check the author of your extension and understand that this user has access to all the permissions listed when you install the extensions. Extensions that have not been updated in a while by the author are more likely to be insecure due to usage of old, insecure APIs. Secondly, don’t trust the prompt that pops up when installing an extension, instead open the manifest file and read the permissions to make sure you really understand what is happening. For example, did you know the popup for an extension with the activeTab permission will not show the presence of the activeTab permission? Understand that, generally speaking, Firefox is less secure due to the lack of requirement for new extensions to have manifest v3, and thus many Firefox extensions are still stuck on v2. If you would like to go further, you can also check the CodeQL queries published at our CodeQL community packs. in order to check for vulnerabilities. The queries will cover all the vulnerabilities mentioned in this article such as XSS, SSRF, API Injection and include additional best practices alerts.

Want to learn more about how GitHub can take the stress out of shipping secure code?
At GitHub Universe 2024, we’ll explore cutting-edge research and best practices in developer-first security—so you can keep your code secure with tools you already know and love.

Related posts