flawopen.com/Teardowns/cve-2023-32558-nodejs-permission-model-fs-escape

● CVE-2023-32558 · CVSS 7.5 · Высокий
Исследования · FlawOpen

Технический разбор: CVE-2023-32558: Node.js Permission Model Filesystem Escape Teardown

Технический анализ исходного кода и защитные инженерные меры для vulnerabilidade: How accessing low-level C++ internal bindings via process.binding('fs') bypassed the experimental --permission --allow-fs-read flags.

💡 Простыми словами (ELI5)

Наглядная аналогия: Imagine a high-security office building with security guards at the front doors checking badges. A worker who doesn't have a badge walks around back, opens the maintenance door marked 'Internal Staff Only', and walks straight into the vault. In Node.js, the developer guarded the public JavaScript functions, but forgot to lock the underlying C++ internal door.

Ключевые понятия и термины

Node.js Permission Model
The experimental --permission CLI flag restricting process capabilities like filesystem, child process, and worker access.
process.binding()
The legacy internal Node.js API that directly exposes native C++ bindings to JavaScript code.
Path Containment
Ensuring file operations remain strictly inside specified directory boundaries.
Defense-in-Depth
Enforcing security checks at the lowest possible layer (native C++ layer) rather than superficial JS wrappers.

Анализ первопричины

Основная причина заключается в невалидированных граничных параметрах в системах с открытым исходным кодом, что приводит к рассинхронизации состояний и обходу средств безопасности.

Пошаговый сценарий атаки

Step 1

Этап атаки: Start Sandboxed Node.js Process

Технический механизм эксплуатации уязвимости: A developer runs untrusted code with node --permission --allow-fs-read=/tmp app.js.

Step 2

Этап атаки: Access Internal Binding

Технический механизм эксплуатации уязвимости: The script invokes const binding = process.binding('fs');.

Step 3

Этап атаки: Bypass High-Level JS Checks

Технический механизм эксплуатации уязвимости: The script calls binding.open('/etc/passwd') directly, bypassing fs.readFile() wrappers.

Step 4

Этап атаки: Arbitrary System File Read

Технический механизм эксплуатации уязвимости: The native C++ binding executes without permission checks, leaking confidential server files.

Исходный код: Уязвимый vs Защищённый вариант

УЯЗВИМАЯ РЕАЛИЗАЦИЯ
// VULNERABLE: src/node_file.cc before patch
static void Open(const FunctionCallbackInfo<Value>& args) {
    Environment* env = Environment::GetCurrent(args);
    
    // ROOT CAUSE:
    // Only verified permissions if called through JS 'fs' module wrappers!
    // Direct callers of process.binding('fs').open() evaded the check!
    const char* path = *Utf8Value(env->isolate(), args[0]);
    int fd = uv_fs_open(..., path, ...);
    args.GetReturnValue().Set(fd);
}
БЕЗОПАСНЫЙ ИСПРАВЛЕННЫЙ ВАРИАНТ
// SECURE: src/node_file.cc patch
static void Open(const FunctionCallbackInfo<Value>& args) {
    Environment* env = Environment::GetCurrent(args);
    
    const char* path = *Utf8Value(env->isolate(), args[0]);
    
    // 1. Enforce permission verification directly inside native C++ binding layer
    if (env->permission()->is_enabled()) {
        if (!env->permission()->is_granted(PermissionScope::kFileSystemRead, path)) {
            THROW_ERR_ACCESS_DENIED(env, "Access to path %s denied by Permission Model", path);
            return;
        }
    }
    
    int fd = uv_fs_open(..., path, ...);
    args.GetReturnValue().Set(fd);
}

Чек-лист по защите системы для инженеров

Источники