Summary
isPathSafe() (added in 6402e4b) only guards sync functions and a handful of early async ones (up to ~line 2616). The vast majority of async, promise, and stream APIs perform raw filesystem syscalls with zero path validation, completely bypassing the sandbox.
Affected APIs
Async / Promise (no isPathSafe check at all)
| Function |
Line |
Syscall |
FS::rmdir() / rmdirPromise() |
2752 / 2791 |
fs::remove() |
FS::rename() / renamePromise() |
2844 / 2885 |
fs::rename() |
FS::copyFile() / copyFilePromise() |
2941 / 2991 |
fs::copy_file() |
FS::open() / openPromise() |
4128 / 4196 |
_open() / open() |
FS::rm() / rmPromise() |
4643 / 4695 |
fs::remove_all() |
FS::cp() / cpPromise() |
4759 / 4800 |
fs::copy() |
FS::mkdtemp() / mkdtempPromise() |
5375 / 5434 |
fs::create_directory() / mkdtemp() |
FS::lutimes() / lutimesPromise() |
5722 / 5798 |
CreateFileW() / lutimes() |
FS::opendir* (all 3 variants) |
6184 / 6194 / 6224 |
fs::directory_iterator() |
FS::readFile() / readFilePromise() |
1869 / 1952 |
std::ifstream |
FS::writeFile() / writeFilePromise() |
2059 / 2123 |
WriteFileW() / std::ofstream |
stat / lstat / chmod / chown / utimes / link / truncate / access / appendFile / realpath / readlink / symlink / fchown / lchown (async + promise) |
various |
various |
readv / writev (async + promise) |
6268 / 6370 / 6444 / 6544 |
pread / pwrite |
Streams (no isPathSafe check)
| Function |
Line |
Syscall |
FS::createReadStream() |
6687 |
_open(path, O_RDONLY) |
FS::createWriteStream() |
6777 |
_open(path, O_CREAT | O_TRUNC) |
Sync (missing check)
| Function |
Line |
Note |
FS::cpSync() |
4964 |
No isPathSafe for src/dest |
Proof of Concept
const fs = require('fs').promises;
// readFileSync is blocked (isPathSafe exists), but readFilePromise is NOT:
await fs.readFile("../../../../etc/passwd"); // ✅ bypassed
await fs.rm("../../../../some-important-dir"); // ✅ deleted
fs.createReadStream("/etc/shadow"); // ✅ read arbitrary
fs.createWriteStream("/tmp/malicious"); // ✅ write arbitrary
await fs.open("C:\Windows\System32\config\SAM", "r"); // ✅ Windows
Root Cause
isPathSafe() is only called in sync functions and a few early-implemented async functions. The pattern was not applied consistently when the remaining ~60 async/promise/stream functions were added.
Suggested Fix
Create a shared helper:
static std::optional<std::string> validateAndGetString(
v8::Isolate* isolate, const v8::Local<v8::Value>& val) {
if (!val->IsString()) {
isolate->ThrowException(v8::Exception::TypeError(
v8::String::NewFromUtf8Literal(isolate, "Path must be a string")));
return std::nullopt;
}
v8::String::Utf8Value path_val(isolate, val);
if (*path_val == nullptr) return std::nullopt;
if (!isPathSafe(*path_val)) {
isolate->ThrowException(v8::Exception::Error(
v8::String::NewFromUtf8(isolate,
"SecurityError: Path validation failed (traversal detected)")
.ToLocalChecked()));
return std::nullopt;
}
return std::string(*path_val);
}
Call this helper in every path-accepting entry point (sync + async + promise + streams) before any filesystem operation.
Summary
isPathSafe()(added in6402e4b) only guards sync functions and a handful of early async ones (up to ~line 2616). The vast majority of async, promise, and stream APIs perform raw filesystem syscalls with zero path validation, completely bypassing the sandbox.Affected APIs
Async / Promise (no
isPathSafecheck at all)FS::rmdir()/rmdirPromise()fs::remove()FS::rename()/renamePromise()fs::rename()FS::copyFile()/copyFilePromise()fs::copy_file()FS::open()/openPromise()_open()/open()FS::rm()/rmPromise()fs::remove_all()FS::cp()/cpPromise()fs::copy()FS::mkdtemp()/mkdtempPromise()fs::create_directory()/mkdtemp()FS::lutimes()/lutimesPromise()CreateFileW()/lutimes()FS::opendir*(all 3 variants)fs::directory_iterator()FS::readFile()/readFilePromise()std::ifstreamFS::writeFile()/writeFilePromise()WriteFileW()/std::ofstreamstat/lstat/chmod/chown/utimes/link/truncate/access/appendFile/realpath/readlink/symlink/fchown/lchown(async + promise)readv/writev(async + promise)Streams (no
isPathSafecheck)FS::createReadStream()_open(path, O_RDONLY)FS::createWriteStream()_open(path, O_CREAT | O_TRUNC)Sync (missing check)
FS::cpSync()isPathSafefor src/destProof of Concept
Root Cause
isPathSafe()is only called in sync functions and a few early-implemented async functions. The pattern was not applied consistently when the remaining ~60 async/promise/stream functions were added.Suggested Fix
Create a shared helper:
Call this helper in every path-accepting entry point (sync + async + promise + streams) before any filesystem operation.