Research
Security News
Malicious npm Packages Inject SSH Backdoors via Typosquatted Libraries
Socket’s threat research team has detected six malicious npm packages typosquatting popular libraries to insert SSH backdoors.
webextension-polyfill
Advanced tools
A lightweight polyfill library for Promise-based WebExtension APIs in Chrome.
The webextension-polyfill npm package provides a standardized API for developing browser extensions that work across different browsers. It offers a promise-based interface to the WebExtension APIs, making it easier to write consistent and maintainable code.
Browser Tabs
This feature allows you to interact with browser tabs. The code sample demonstrates how to get the current active tab in the current window.
const browser = require('webextension-polyfill');
async function getCurrentTab() {
let tabs = await browser.tabs.query({ active: true, currentWindow: true });
return tabs[0];
}
getCurrentTab().then(tab => console.log(tab));
Storage
This feature allows you to store and retrieve data using the browser's storage API. The code sample demonstrates how to save a key-value pair to local storage and then retrieve it.
const browser = require('webextension-polyfill');
async function saveToStorage(key, value) {
await browser.storage.local.set({ [key]: value });
}
async function getFromStorage(key) {
let result = await browser.storage.local.get(key);
return result[key];
}
saveToStorage('exampleKey', 'exampleValue');
getFromStorage('exampleKey').then(value => console.log(value));
Messaging
This feature allows you to send messages between different parts of your extension, such as between a background script and a content script. The code sample demonstrates how to set up a message listener in the background script and send a message from the content script.
const browser = require('webextension-polyfill');
// Background script
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.greeting === 'hello') {
sendResponse({ response: 'hi' });
}
});
// Content script
browser.runtime.sendMessage({ greeting: 'hello' }).then(response => {
console.log(response.response);
});
The chrome-extension-async package provides a promise-based interface for Chrome's extension APIs. It is similar to webextension-polyfill but is specific to Chrome, whereas webextension-polyfill aims to be cross-browser.
browser
API PolyfillThis library allows extensions that use the Promise-based WebExtension/BrowserExt API being standardized by the W3 Browser Extensions group to run on Google Chrome with minimal or no changes.
This library doesn't (and it is not going to) polyfill API methods or options that are missing on Chrome but natively provided on Firefox, and so the extension has to do its own "runtime feature detection" in those cases (and then eventually polyfill the missing feature on its own or enable/disable some of the features accordingly).
Browser | Support Level |
---|---|
Chrome | Officially Supported (with automated tests) |
Firefox | Officially Supported as a NO-OP (with automated tests for comparison with the behaviors on Chrome) |
Opera / Edge (>=79.0.309) | Unofficially Supported as a Chrome-compatible target (but not explicitly tested in automation) |
The polyfill is being tested explicitly (with automated tests that run on every pull request) on officially supported browsers (that are currently the last stable versions of Chrome and Firefox).
On Firefox, this library is actually acting as a NO-OP: it detects that the browser
API object is already defined
and it does not create any custom wrappers.
Firefox is still included in the automated tests, to ensure that no wrappers are being created when running on Firefox,
and for comparison with the behaviors implemented by the library on Chrome.
A new version of the library is built from this repository and released as an npm package.
The npm package is named after this repo: webextension-polyfill.
For the extension that already include a package.json file, the last released version of this library can be quickly installed using:
npm install --save-dev webextension-polyfill
Inside the dist/
directory of the npm package, there are both the minified and non-minified builds (and their related source map files):
For extensions that do not include a package.json file and/or prefer to download and add the library directly into their own code repository, all the versions released on npm are also available for direct download from unpkg.com:
and linked to the Github releases:
In order to use the polyfill, it must be loaded into any context where browser
APIs are accessed. The most common cases
are background and content scripts, which can be specified in manifest.json
(make sure to include the browser-polyfill.js
script before any other scripts that use it):
{
// ...
"background": {
"scripts": [
"browser-polyfill.js",
"background.js"
]
},
"content_scripts": [{
// ...
"js": [
"browser-polyfill.js",
"content.js"
]
}]
}
For HTML documents, such as browserAction
popups, or tab pages, it must be
included more explicitly:
<!DOCTYPE html>
<html>
<head>
<script type="application/javascript" src="browser-polyfill.js"></script>
<script type="application/javascript" src="popup.js"></script>
</head>
<!-- ... -->
</html>
And for dynamically-injected content scripts loaded by tabs.executeScript
,
it must be injected by a separate executeScript
call, unless it has
already been loaded via a content_scripts
declaration in
manifest.json
:
browser.tabs.executeScript({file: "browser-polyfill.js"});
browser.tabs.executeScript({file: "content.js"}).then(result => {
// ...
});
The polyfill can also be loaded using the native ES6 module loader available in the recent browsers versions.
Be aware that the polyfill module does not export the browser
API object,
but defines the browser
object in the global namespace (i.e. window
).
<!DOCTYPE html>
<html>
<head>
<script type="module" src="browser-polyfill.js"></script>
<script type="module" src="background.js"></script>
</head>
<!-- ... -->
</html>
// In background.js (loaded after browser-polyfill.js) the `browser`
// API object is already defined and provides the promise-based APIs.
browser.runtime.onMessage.addListener(...);
This library is built as a UMD module (Universal Module Definition), and so it can also be used with module bundlers (and explicitly tested on both webpack and browserify) or AMD module loaders.
src/background.js:
var browser = require("webextension-polyfill");
browser.runtime.onMessage.addListener(async (msg, sender) => {
console.log("BG page received message", msg, "from", sender);
console.log("Stored data", await browser.storage.local.get());
});
browser.browserAction.onClicked.addListener(() => {
browser.tabs.executeScript({file: "content.js"});
});
src/content.js:
var browser = require("webextension-polyfill");
browser.storage.local.set({
[window.location.hostname]: document.title,
}).then(() => {
browser.runtime.sendMessage(`Saved document title for ${window.location.hostname}`);
});
By using require("webextension-polyfill")
, the module bundler will use the non-minified version of this library, and the extension is supposed to minify the entire generated bundles as part of its own build steps.
If the extension doesn't minify its own sources, it is still possible to explicitly ask the module bundler to use the minified version of this library, e.g.:
var browser = require("webextension-polyfill/dist/browser-polyfill.min");
...
The previous section explains how to bundle webextension-polyfill
in each script. An alternative method is to include a single copy of the library in your extension, and load the library as shown in Basic Setup. You will need to install copy-webpack-plugin:
npm install --save-dev copy-webpack-plugin
In webpack.config.js
, import the plugin and configure it this way. It will copy the minified file into your output folder, wherever your other webpack files are generated.
const CopyWebpackPlugin = require('copy-webpack-plugin');
module.exports = {
/* Your regular webpack config, probably including something like this:
output: {
path: path.join(__dirname, 'distribution'),
filename: '[name].js'
},
*/
plugins: [
new CopyWebpackPlugin({
patterns: [{
from: 'node_modules/webextension-polyfill/dist/browser-polyfill.js',
}],
})
]
}
And then include the file in each context, using the manifest.json
just like in Basic Setup.
The Promise-based APIs in the browser
namespace work, for the most part,
very similarly to the callback-based APIs in Chrome's chrome
namespace.
The major differences are:
Rather than receiving a callback argument, every async function returns a
Promise
object, which resolves or rejects when the operation completes.
Rather than checking the chrome.runtime.lastError
property from every
callback, code which needs to explicitly deal with errors registers a
separate Promise rejection handler.
Rather than receiving a sendResponse
callback to send a response,
onMessage
listeners simply return a Promise whose resolution value is
used as a reply.
Rather than nesting callbacks when a sequence of operations depend on each other, Promise chaining is generally used instead.
The resulting Promises can be also used with async
and await
, rather
than dealt with directly.
The following code will retrieve a list of URLs patterns from the storage
API, retrieve a list of tabs which match any of them, reload each of those
tabs, and notify the user that is has been done:
browser.storage.local.get("urls").then(({urls}) => {
return browser.tabs.query({url: urls});
}).then(tabs => {
return Promise.all(
Array.from(tabs, tab => browser.tabs.reload(tab.id))
);
}).then(() => {
return browser.notifications.create({
type: "basic",
iconUrl: "icon.png",
title: "Tabs reloaded",
message: "Your tabs have been reloaded",
});
}).catch(error => {
console.error(`An error occurred while reloading tabs: ${error.message}`);
});
Or, using an async function:
async function reloadTabs() {
try {
let {urls} = await browser.storage.local.get("urls");
let tabs = await browser.tabs.query({url: urls});
await Promise.all(
Array.from(tabs, tab => browser.tabs.reload(tab.id))
);
await browser.notifications.create({
type: "basic",
iconUrl: "icon.png",
title: "Tabs reloaded",
message: "Your tabs have been reloaded",
});
} catch (error) {
console.error(`An error occurred while reloading tabs: ${error.message}`);
}
}
It's also possible to use Promises effectively using two-way messaging. Communication between a background page and a tab content script, for example, looks something like this from the background page side:
browser.tabs.sendMessage(tabId, "get-ids").then(results => {
processResults(results);
});
And like this from the content script:
browser.runtime.onMessage.addListener(msg => {
if (msg == "get-ids") {
return browser.storage.local.get("idPattern").then(({idPattern}) => {
return Array.from(document.querySelectorAll(idPattern),
elem => elem.textContent);
});
}
});
or:
browser.runtime.onMessage.addListener(async function(msg) {
if (msg == "get-ids") {
let {idPattern} = await browser.storage.local.get("idPattern");
return Array.from(document.querySelectorAll(idPattern),
elem => elem.textContent);
}
});
Or vice versa.
There are multiple projects that add TypeScript support to your web-extension project:
Project | Description |
---|---|
@types/webextension-polyfill | Types and JS-Doc are automatically generated from the mozilla schema files, so it is always up-to-date with the latest APIs. Formerly known as webextension-polyfill-ts. |
web-ext-types | Manually maintained types based on MDN's documentation. No JS-Doc included. |
@types/chrome | Manually maintained types and JS-Doc. Only contains types for chrome extensions though! |
This library tries to minimize the amount of "special handling" that a cross-browser extension has to do to be able to run on the supported browsers from a single codebase, but there are still cases when polyfillling the missing or incompatible behaviors or features is not possible or out of the scope of this polyfill.
This section aims to keep track of the most common issues that an extension may have.
While some of the asynchronous API methods in Firefox (the ones that return a promise) also support the callback parameter (mostly as a side effect of the backward compatibility with the callback-based APIs available on Chrome), the Promise-based APIs provided by this library do not support the callback parameter (See "#102 Cannot call browser.storage.local.get with callback").
This library takes its knowledge of the APIs to wrap and their signatures from a metadata JSON file: api-metadata.json.
If an API method is not yet included in this "API metadata" file, it will not be recognized. Promises are not supported for unrecognized APIs, and callbacks have to be used for them.
Chrome-only APIs have no promise version, because extensions that use such APIs would not be compatible with Firefox.
File an issue in this repository for API methods that support callbacks in Chrome and Firefox but are currently missing from the "API metadata" file.
When an extension that uses this library doesn't behave as expected on Firefox, it is almost never an issue in this polyfill, but an issue with the native implementation in Firefox.
"Firefox only" issues should be reported upstream on Bugzilla:
This library does not provide any polyfill for API methods and options that are only available on Firefox, and they are actually considered out of the scope of this library.
On Firefox browser.tabs.executeScript
returns a promise which resolves to the result of the content script code that has been executed, which can be an immediate value or a Promise.
On Chrome, the browser.tabs.executeScript
API method as polyfilled by this library also returns a promise which resolves to the result of the content script code, but only immediate values are supported.
If the content script code result is a Promise, the promise returned by browser.tabs.executeScript
will be resolved to undefined
.
MSEdge versions >= 79.0.309 are unofficially supported as a Chrome-compatible target (as for Opera or other Chrome-based browsers that also support extensions).
MSEdge versions older than 79.0.309 are unsupported, for extension developers that still have to work on extensions for older MSEdge versions, the MSEdge --ms-preload
manifest key and the Microsoft Edge Extension Toolkit's Chrome API bridge can be used to be able to load the webextension-polyfill without any MSEdge specific changes.
The following Github repository provides some additional detail about this strategy and a minimal test extension that shows how to put it together:
Read the contributing section for additional information about how to build the library from this repository and how to contribute and test changes.
FAQs
A lightweight polyfill library for Promise-based WebExtension APIs in Chrome.
The npm package webextension-polyfill receives a total of 334,863 weekly downloads. As such, webextension-polyfill popularity was classified as popular.
We found that webextension-polyfill demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Research
Security News
Socket’s threat research team has detected six malicious npm packages typosquatting popular libraries to insert SSH backdoors.
Security News
MITRE's 2024 CWE Top 25 highlights critical software vulnerabilities like XSS, SQL Injection, and CSRF, reflecting shifts due to a refined ranking methodology.
Security News
In this segment of the Risky Business podcast, Feross Aboukhadijeh and Patrick Gray discuss the challenges of tracking malware discovered in open source softare.