SecAdvent
Busting Browser Security with Extensions – SecAdvent Day 18
December 18, 2020
These days, many of us spend a good part of our time in a web browser. This includes everyone from my mom who is a retired nurse to me, a software developer, and everyone between.
My mom used to be an easy target for phishing emails and clickbait links (including downloads), resulting in malware and viruses on her computer. She knows better now.
But, what about browser extensions? There are great legitimate browser extensions that I use everyday. They do everything from making sure my grammar is correct as I type this post, to making it easier for me to login with password vault software.
The fact is, it is shockingly easy to get a browser extension listed on official outlets with very little code review or understanding of what’s going on inside them.
As part of this post, I gave myself a challenge: could I write a browser extension that had broad authority over the browser, write it in a cross-platform way, and get that browser extension listed on both the Google and Mozilla extension markets?
SPOILER ALERT: THE ANSWER IS YES, YES (THANKS TO WEB-EXT) AND YES! (LINKS AT THE END).
What the heck is WEB-EXT?
In the dark ages of a few years ago, if you wanted to write a browser extension for Google Chrome and for Mozilla Firefox, you had to have two separate code bases.
Those codebases would be very close in nature, asboth Firefox and Chrome use JavaScript for their extensions,and they both have similar ways to handle events and popups.
Cue the web-ext project from Mozilla. It’s a command line tool that lets you build, run and test cross-platform browser extensions.
In addition to making it super easy to see changes in real-time, it adds a small shim – called a polyfill – that makes it so that you can use all the same calls for Chrome and Firefox. Pretty handy, eh?
To get started, you’ll need to have node.js installed. Then, install web-ext globally:
npm install --global web-ext
The finished browser extension code used in this post can be found on my GitHub. It’s called: Pretty Kitty.
Anatomy of a browser extension
A browser extension starts with a manifest.json file. This file describes three primary parts of the extension: background scripts, popup scripts, and content altering scripts.
Take a look at a (scaled down) manifest.json script for the project:
{
...
"name": "Pretty Kitty",
...
"permissions": [
"*://*/*",
"webRequest", "webRequestBlocking",
"tabs", "storage"
],
"background": {
"scripts": [
"browser-polyfill.min.js",
"background.js"
]
},
"content_scripts": [
{
"matches": ["*://*/*"],
"js": [
"browser-polyfill.min.js",
"beautify.js"
]
}
],
"browser_action": {
...
"default_popup": "popup/index.html"
}
}
There’s a lot going on here, so let’s unpack it.
The permissions section describes what elements of the browser the extension will have access to. The first parameter is a regex that describes which urls the following permissions will be applied to. Notice that in this example ALL urls will be matched. The first two permissions are webRequest and webRequestBlocking. This gives the extension access to intercept all requests and to block completion of the request pending code executed in the extension. The next two permissions ask for access to all tabs on the browser and to local storage.
The background section references scripts that will – you got it – run in the background. Notice that this section references both the polyfill for cross-platform compatibility as well as the code: background.js.
The content scripts section describes a regex for which urls the code will be executed . Notice that the value I’ve supplied will match any and all urls visited. This is a huge amount of authority the extension is asking of the browser. It’s basically saying: “For every url visited, execute the content-altering code: beautify.js”. The content altering code could be doing something very nefarious. It could, for instance, replace all links on the page with proxy links that make you visit a different site thanthen the one you intended to.
The browser action section describes a popup that will overlay a window underneath the extension icon.
TL;DR: the manifest for this browser extension is asking for the broadest possible permissions. If installed, this extension will be able to intercept and alter ALL requests and will be able to intercept and alter ALL responses.
Before we jump into all the code described in the manifest.json file, let’s take a look at how we use web-ext to run the project locally and build for distribution to the Google Web Store and the Mozilla Add-ons store. NOTE: Mozilla uses both the terms add-ons and extensions. They are synonymous.
Like many projects managed with npm, we’ll start this one with a package.json file:
{
"name": "pretty-kitty",
"description": "A Browser extension to show gifs of cats when you should be working",
"version": "0.1.1",
"scripts": {
"build": "web-ext build"
},
...
"webExt": {
"verbose": true,
"build": {
"overwriteDest": true
},
"ignoreFiles": [
"package.json",
"README.md"
]
}
}
The important bits here are the scripts section which includes a command to build the project for distribution. Also, the ignoreFiles section is handy to ensure that certain files don’t end up in the distribution archive.
In this case, we’re simply using npm and package.json to make building and running easy. There’s not actually any node.js in the project.
When your code is ready, you run:
npm run build
This prepares it for submission to the browser extension stores.
The fact is, it is shockingly easy to get a browser extension listed on official outlets with very little code review or understanding of what’s going on inside them.
Who’s a pretty kitty?
One of my goals in this exercise was to prove that my browser extension could have done malicious behavior without it actually doing malicious behavior.
To accomplish this, the extension masquerades as something that just shows you cat gifs. But, it also gives you the opportunity to alter pages in a way that demonstrates it has complete control over the browser. It does this by having a couple of checkbox controls in a popup. A really nefarious browser extension wouldn’t have friendly controls to intercept urls or alter content on the page – it would just do it.
By way of example, here’s a view of a website without the checkboxes checked.
Now, here’s a view with the checkboxes checked:
All of the ‘e’s have been replaced with ‘ë’s and all of the images have been replaced with cat pictures. And, if you look carefully, you’ll see that the pictures have been replaced with cat pictures that match the dimensions of the original image.
Let’s dig into the code and see what’s going on here.
We’ll start with the popup. That’s what drives what you see when you click on the icon on the browser’s extension bar. It’s worth noting that browser extensions don’t have to have a popup at all. In this case, I wanted to demonstrate the full capabilities of browser extensions.
Here’s popup/index.html (abbreviated):
Pretty straightforward, right?
It’s using an API called thecatapi.com. It will always return a gif. So, when you click the kitty-cat icon in the browser bar, the popup will display a new gif from the API. It’s important to note that the popup is always reloaded each time you click the icon. You have to carefully manage state. You may be wondering how we’re going to ensure that the checkboxes stay checked because of this. Let’s take a look at one of the supporting files to see what’s going on here: support.js.
function updateSettings(itemName) {
browser.storage.local.get('pretty.kitty.settings')
.then(item => {
var update = item['pretty.kitty.settings'];
update[itemName] = !update[itemName];
browser.storage.local.set({'pretty.kitty.settings': update})
.then(() => console.log("OK"));
});
}
document.getElementById('es').addEventListener('click', function () {
updateSettings('replaceEs');
});
document.getElementById('imgs').addEventListener('click', function () {
updateSettings('replaceImgs');
});
browser.storage.local.get('pretty.kitty.settings')
.then(item => {
var settings = item['pretty.kitty.settings'];
document.getElementById('es').checked = settings.replaceEs;
document.getElementById('imgs').checked = settings.replaceImgs;
});
Let’s work our way from the bottom up
When the popup is loaded, local storage is accessed to get a handle to something called: pretty.kitty.settings. This is an object that maintains the state of the checkboxes. Assuming that local storage contains this object, there are two properties: replaceEs and replaceImgs. Both are boolean values and the matching checkboxes are checked according to their values.
Moving up, you see that for each of the checkboxes, we’ve set up a click event listener. When triggered, the updateSettings function is called.
Further up, the updateSettings function uses local storage once again to toggle the setting based on the checkbox.
You may be wondering how the pretty.kitty.settings object gets initialized in the first place. To understand that, let’s next take a look at a section of the background.js script:
browser.storage.local.get('pretty.kitty.settings')
.then(item => {
if (!item['pretty.kitty.settings']) {
browser.storage.local.set({
'pretty.kitty.settings': {
replaceEs: false,
replaceImgs: false
}
}).then(() => console.log("OK"));
}
});
If the local storage does not yet have a pretty.kitty.settings object, it’s initialized with false values for both <replaceEs and replaceImgs. This matches the starting state of the checkboxes in the popup.
Here’s some more of background.js:
browser.storage.onChanged.addListener(changes => {
browser.tabs.query({ active: true, currentWindow: true})
.then(activeTabs => {
browser.tabs.reload(activeTabs[0].id);
})
});
This snippet listens for changes to local storage. When changed, it reloads the page on the current tab. This ensures that if you’ve checked or unchecked the checkboxes, the content-altering code will kick in when the page is reloaded. We’ll look at two more elements of the background.js script before we switch over to the content-altering script.
First, I will demonstrate how the background script can alter ALL requests:
browser.webRequest.onBeforeSendHeaders.addListener(
function (details) {
details.requestHeaders.push({name: 'x-nothing', value: 'to-see-here'})
return {
requestHeaders: details.requestHeaders
};
},
{urls: ["https://*/*"], types: ["main_frame"]},
["blocking", "requestHeaders"]
);
In this case, a header is being added to the initial page request before the request is made. Notice the types array just has: “main_frame”. I could have added an additional header to every single request, including script, images, css, etc. But, I didn’t want to slow things down, so it only affects the initial request. Here’s the point:
My initial request to a web page now includes a new header: x-nothing with value: to-see-here. Ordinary users would be completely unaware of this behavior. A malicious browser extension might try to set additional headers that would trigger unknown or unwelcome behavior on other sites.
The second behavior I want to show from background.js is this:
browser.webRequest.onBeforeRequest.addListener(
function (details) {
console.log(details.url.replace(/\?injected_param=uh_oh/g,'') + '?injected_param=uh_oh');
// return {
// redirectUrl: details.url.replace(/\?injected_param=uh_oh/g,'') + '?injected_param=uh_oh'
// }
return {}
},
{urls: ["https://*/*"], types: ["main_frame"]},
["blocking"]
);
In this case, code is being executed before the request is made. For the purposes of demonstration, I am just logging out an alteration of the request. I didn’t want to introduce potentially breaking changes to every request being made by the browser, especially as one of my goals was to get this extension publicly submitted to the extension stores.
All of this is happening behind the scenes, unbeknownst to the user who has installed the extension.
The last element of the browser extension we’ll look at is the part that’s most visible to users: the content-altering script: beautify.js.
browser.storage.local.get('pretty.kitty.settings')
.then(item => {
if (item['pretty.kitty.settings'].replaceEs) {
for (elem of document.getElementsByTagName('a')) {
elem.textContent = elem.textContent.replace(/e/g,'ë');
}
}
if (item['pretty.kitty.settings'].replaceImgs) {
for (elem of document.getElementsByTagName('img')) {
elem.src = 'https://placekitten.com/' + elem.width + '/' + elem.height;
}
}
})
This is the simplest (and yet most visually impactful) part of the extension. Based on the values set in local storage, the entire page will be altered. I wanted to keep this code small and easy to understand. If replaceEs is set, all links on the page (<a href=…>) will have its text updated to replace ‘e’s with ‘ë’s. If repalceImgs is set, each image on the page is replaced with a cat picture that matches the original image’s dimensions. This is accomplished using another api called placekitten.com.
Publishing a browser exsention
I submitted this browser extension – with all the code shown above – to both of the primary extension stores. And, tada – after less than 24 hours, both were listed. They are both currently publicly available.
THE GOOGLE CHROME VERSION OF THE EXTENSION CAN BE FOUND HERE.
THE MOZILLA FIREFOX VERSION OF THE EXTENSION CAN BE FOUND HERE.
Go ahead and add it to your browser! It won’t do anything malicious. Or, will it? (No it won’t! That’s why I kept it open source).
Getting ready to publish with web-ext is super easy. Just run:
npm run build
web-ext reads the manifest and builds a zip file that’s suitable for publishing both in the Google Chrome Web store and the Mozilla Firefox Add-ons store.
I go into a lot more detail about submitting the extension to the various stores in this video.
So long, pretty kitty
I hope this tour of browser extensions has been helpful. Maybe you’ve learned some of the bones of what goes into an extension.
My hope is that you’ve seen that browser extensions represent a potent vector of attack and one that is often overlooked.
Browser extensions can have broad authority over your browser – easily violating rules that ordinary web apps have to abide by – like cross-domain access to local storage. Additionally, it is way too easy to get browser extensions publicly listed and available on the web stores.
Developers, DevOps, DevSecOps and security professionals of all stripes need to be aware of this sleeper vector of attack.
About Micah Silverman
Micah Silverman is a Senior Developer Advocate for Okta. With 24 years of Java Experience (yup, that’s from the beginning), he’s authored numerous articles, co-authored a Java EE book and spoken at many conferences. He’s a maker, who’s built full size MAME arcade cabinets and repaired old electronic games (http://afitnerd.com/2011/10/16/weekend-project-fix-dark-tower/). He brings his love of all things Java and Developer Evangelism to a conference near you!