Skip to content

[Security][Critical] Path traversal bypass — async/promise/stream APIs skip isPathSafe() #16

Description

@JkarVN

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingcriticalPriority: criticalsecuritySecurity vulnerability

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions