userland.run70Try the demo
Example · boa-sandbox

Sandboxed JavaScript with capability grants (Boa)

boa · sandboxed JSriscv · RISC-V VM

The boa runner is the Boa JavaScript interpreter compiled to WebAssembly. A script evaluated with nano.script() gets a nano global whose powers are exactly what expose grants: file access (none / readonly / readwrite), run (BusyBox on the RISC-V VM), node, and a short list of web APIs. Anything not granted does not exist inside the sandbox.

This script lists and counts lines in a project directory, runs wc -l through the granted run capability (a BusyBox applet on the RISC-V VM — a cross-tier call), and shows that fetch is absent.

Run it

Boa sandbox

Run the sandboxed script

Evaluated by Boa with fs + run granted. Try calling fetch(...) — it is not defined.

javascript
// Runs in the Boa sandbox (nano.script). This engine was granted the `fs` and
// `run` capabilities plus console — nothing else: no network, no DOM, no host
// JavaScript objects. The value of the last expression (awaited if it is a
// promise) comes back to the page as JSON.
(async () => {
  const dir = "/work/project";
  const files = nano.fs.list(dir).map((f) => f.name).sort();
  console.log("files:", files.join(", "));

  let total = 0;
  for (const name of files) {
    const lines = nano.fs.readText(dir + "/" + name).split("\n").filter(Boolean).length;
    total += lines;
    console.log(name.padEnd(12), lines, "lines");
  }
  console.log("total:", total, "lines");

  // `run` is a granted capability: a BusyBox applet on the RISC-V VM.
  const wc = await nano.run("wc -l " + dir + "/readme.md");
  console.log("busybox wc -l:", wc.stdout.trim());

  // Not granted: the global simply doesn't exist inside the sandbox.
  console.log("typeof fetch:", typeof fetch);

  return { files: files.length, total };
})();
loading…
Expected output
files: app.js, readme.md, util.js
app.js       2 lines
readme.md    3 lines
util.js      2 lines
total: 7 lines
busybox wc -l: 3 /work/project/readme.md
typeof fetch: undefined
→ {
  "files": 3,
  "total": 7
}

How it runs

  1. createNano({ scripting: { wasm: "/nano/boa.wasm" } }) — boa.wasm loads on the first script.
  2. The three project files are written to /work/project/.
  3. nano.script(source, { expose: { fs: "readwrite", run: true, webapis: ["console", …] }, limits, timeoutMs }) evaluates the script; console output streams to the pane and the final value comes back as JSON.

Source

From demos/boa-sandbox/.

script.js
javascript
// Runs in the Boa sandbox (nano.script). This engine was granted the `fs` and
// `run` capabilities plus console — nothing else: no network, no DOM, no host
// JavaScript objects. The value of the last expression (awaited if it is a
// promise) comes back to the page as JSON.
(async () => {
  const dir = "/work/project";
  const files = nano.fs.list(dir).map((f) => f.name).sort();
  console.log("files:", files.join(", "));

  let total = 0;
  for (const name of files) {
    const lines = nano.fs.readText(dir + "/" + name).split("\n").filter(Boolean).length;
    total += lines;
    console.log(name.padEnd(12), lines, "lines");
  }
  console.log("total:", total, "lines");

  // `run` is a granted capability: a BusyBox applet on the RISC-V VM.
  const wc = await nano.run("wc -l " + dir + "/readme.md");
  console.log("busybox wc -l:", wc.stdout.trim());

  // Not granted: the global simply doesn't exist inside the sandbox.
  console.log("typeof fetch:", typeof fetch);

  return { files: files.length, total };
})();
project/app.js
javascript
const answer = 42;
export const A = answer;
project/util.js
javascript
export const sum = (a, b) => a + b;
export const PI = 3.14159;
project/readme.md
markdown
# demo
sandboxed scripting
over the VM's files

Further reading

Other examples