Skip to content

fs.cpSync copies symlinks without permission check on target path #63179

@fg0x0

Description

@fg0x0

Summary

fs.cpSync with recursive:true calls std::filesystem::create_symlink() in CpSyncCopyDir (src/node_file.cc:3819-3827) without checking if the symlink target is within the allowed permission paths.

fs.symlinkSync already has THROW_IF_INSUFFICIENT_PERMISSIONS checks on the target (added at node_file.cc:1353-1357). The same check is missing in cpSync symlink copy path and the verbatimSymlinks path (~line 3754).

Repro

mkdir -p /tmp/allowed/src /tmp/allowed/dest /tmp/denied
echo SECRET > /tmp/denied/secret.txt
ln -sf /tmp/denied/secret.txt /tmp/allowed/src/link

node --permission --allow-fs-read=/tmp/allowed --allow-fs-write=/tmp/allowed --allow-fs-read=/usr --allow-fs-read=/lib -e '
const fs = require("node:fs");
try { fs.readFileSync("/tmp/denied/secret.txt"); } catch(e) { console.log("blocked:", e.code); }
try { fs.symlinkSync("/tmp/denied/secret.txt", "/tmp/allowed/dest/link"); } catch(e) { console.log("blocked:", e.code); }
fs.cpSync("/tmp/allowed/src/", "/tmp/allowed/dest/", { recursive: true });
console.log("read:", fs.readFileSync("/tmp/allowed/dest/link", "utf8").trim());
'

Output

blocked: ERR_ACCESS_DENIED
blocked: ERR_ACCESS_DENIED
read: SECRET

Suggested fix

Add THROW_IF_INSUFFICIENT_PERMISSIONS for both kFileSystemRead and kFileSystemWrite on symlink_target_absolute before the create_symlink / create_directory_symlink calls in CpSyncCopyDir, and similarly in the copy_symlink path.

Discussed with @RafaelGSS - confirmed as valid for hardening.
Tested on Node.js v24.14.1.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions