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.
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
Output
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.