eval() with WASM instead of sandbox with Web Extensions

ergofriend

kasu

Posted on October 30, 2024

eval() with WASM instead of sandbox with Web Extensions

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';"
Enter fullscreen mode Exit fullscreen mode

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.

GitHub logo 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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

CleanShot 2024-10-13 at 15 05 53@2x


This article is a translation from Japanese.
https://ergofriend.hatenablog.com/entry/2024/10/17/005830


  1. ergofriend/Quiqsearch: A chrome extension. Quickly search for the Selected text on the Web. 

  2. pokutuna/chrome-cocopy: chrome extension to copy text by your code. 

  3. eval() - JavaScript | MDN 

  4. Browser compatibility : sandbox - Mozilla | MDN 

  5. bellard/quickjs: Public repository of the QuickJS Javascript Engine. 

💖 💪 🙅 🚩
ergofriend
kasu

Posted on October 30, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related