in 4 lines
- Manifest V3 is now required for
eval()
in Chrome extensions, so you need to use sandbox. - Firefox without sandbox can use
unsafe-eval
by reverting to V2... - Chrome and Firefox have different implementations, and I don't want to have a security gap between the two :(
- Yes, let's succes with QuickJS(WASM)!
Background
I am working on a Web Extension1.
What this extension provided was Google search and YouTube/X site search. These implementations were hard-coded.
We wanted to avoid having to make a release for every variation, so we were looking for a means for users to set up their own arbitrary site search or favorite search engine.
Policy
Based on the above background, we have decided to provide a function that allows users to register a function that can be used to process and copy URLs and page content, such as in Cocopy2. We decided to provide a function that allows users to register functions they have entered.
Basically, the function entered by the user is in string form, and the browser has a function called eval()
3 is built in.
However, extensions are limited by content_security_policy - Mozilla | MDN.
Extension eval()
Fact
First, the extension has a version specification called Manifest.
Chrome is only available in V3, while Firefox currently allows you to choose between V2 and V3, whichever you prefer.
The basic eval()
method differs depending on the version.
Manifest V3
Using eval() in a sandboxed iframe | Chrome for Developers
V3 does not allow direct use of eval()
, so the method via sandbox is guided.
However, this is not the case in Firefox4.
Manifest V2
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self';"
In V2, eval()
can be used by specifying unsafe-eval
.
However, as the dangers of eval()
are widely known, it is a method to be avoided if at all possible.
Here's what's wrong with the general method
Chrome requires V3, so it is sandboxed, and Firefox does not have a sandbox, so it is naturally fixed to the unsafe-eval
implementation in V2.
- Different implementations between Chrome and Firefox can be a maintenance cost
- Security gap between sandbox and unsafe-eval
WASM may be an option.
There is a JavaScript engine called QuickJS5 made in C. engine made in C.
If this can be called from an extension as a WASM Module, the same implementation can be used in both Chrome and Firefox, and security can be ensured.
Implementation
Thankfully, a WASM build of QuickJS and a client for the JavaScript environment were available, so we were able to use these.
justjake / quickjs-emscripten
Safely execute untrusted Javascript in your Javascript, and execute synchronous code that uses async functions
quickjs-emscripten
Javascript/Typescript bindings for QuickJS, a modern Javascript interpreter compiled to WebAssembly.
- Safely evaluate untrusted Javascript (supports most of ES2023).
- Create and manipulate values inside the QuickJS runtime (more).
- Expose host functions to the QuickJS runtime (more).
- Execute synchronous code that uses asynchronous functions, with asyncify.
- Supports browsers, NodeJS, Deno, Bun, Cloudflare Workers, QuickJS (via quickjs-for-quickjs).
Github | NPM | API Documentation | Variants | Examples
import { getQuickJS } from "quickjs-emscripten"
async function main() {
const QuickJS = await getQuickJS()
const vm = QuickJS.newContext()
const world = vm.newString("world")
vm.setProp(vm.global, "NAME", world)
world.dispose()
const result = vm.evalCode(`"Hello " + NAME + "!"`)
if (result.error) {
console.log
…The default method in the documentation fetches the WASM Module remotely, so the extension must import it directly to bundle the main body.
import variant from "@jitl/quickjs-singlefile-browser-release-sync"
import { newQuickJSWASMModuleFromVariant } from "quickjs-emscripten-core"
const QuickJS = await newQuickJSWASMModuleFromVariant(variant)
console.log(QuickJS.evalCode("1 + 1")) // 2
For user input, we asked the user to define an anonymous function, which was immediately executed and evaluated.
const inputCode = `
({keyword}) => {
return "https://www.google.com/search?q=" + keyword
}`
const result = QuickJS.evalCode(`(${inputCode})({ keyword: "Golang"})`)
expect(result).toBe("https://www.google.com/search?q=Golang")
Reference Implementation
This is a pull request for actual incorporation into the extension.
Add custom filter #10
I have improved the search methods for user.
- [x] Browser's default search engine
- [x] Building custom search url
- [x] Escape hatch for custom filters
- [x] Sorting custom filters
This article is a translation from Japanese.
https://ergofriend.hatenablog.com/entry/2024/10/17/005830
Top comments (0)