Skip to content

Commit b7a57bf

Browse files
author
fg0x0
committed
permission: check symlink target in cpSync
fs.cpSync with recursive:true calls create_symlink() and copy_symlink() without checking if the symlink target is within the allowed permission paths. fs.symlinkSync already validates symlink targets against the permission model (added as the fix for CVE-2025-55130 at src/node_file.cc:1353-1357). The same check was missing in CpSyncCopyDir for both the standard symlink copy path and the verbatimSymlinks code path. Add permission checks for both kFileSystemRead and kFileSystemWrite on the resolved symlink target before create_symlink, create_directory_symlink, and copy_symlink calls in CpSyncCopyDir. Fixes: #63179 Refs: CVE-2025-55130
1 parent bbf51ad commit b7a57bf

2 files changed

Lines changed: 129 additions & 0 deletions

File tree

src/node_file.cc

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3752,6 +3752,24 @@ static void CpSyncCopyDir(const FunctionCallbackInfo<Value>& args) {
37523752

37533753
if (dir_entry.is_symlink()) {
37543754
if (verbatim_symlinks) {
3755+
3756+
// Permission check for verbatimSymlinks path (incomplete CVE-2025-55130 fix)
3757+
if (env->permission()->enabled()) {
3758+
auto verb_target = std::filesystem::read_symlink(src, error);
3759+
if (error) break;
3760+
auto verb_target_abs = std::filesystem::weakly_canonical(
3761+
std::filesystem::absolute(src.parent_path() / verb_target));
3762+
auto verb_str = verb_target_abs.string();
3763+
auto verb_view = std::string_view(verb_str);
3764+
if (!env->permission()->is_granted(
3765+
env, permission::PermissionScope::kFileSystemRead, verb_view) ||
3766+
!env->permission()->is_granted(
3767+
env, permission::PermissionScope::kFileSystemWrite, verb_view)) {
3768+
return THROW_ERR_ACCESS_DENIED(env,
3769+
"Access to symlink target '%s' denied", verb_str.c_str());
3770+
}
3771+
}
3772+
37553773
std::filesystem::copy_symlink(
37563774
dir_entry.path(), dest_file_path, error);
37573775
if (error) {
@@ -3818,6 +3836,24 @@ static void CpSyncCopyDir(const FunctionCallbackInfo<Value>& args) {
38183836
}
38193837
auto symlink_target_absolute = std::filesystem::weakly_canonical(
38203838
std::filesystem::absolute(src / symlink_target));
3839+
3840+
// Permission check for symlink target (incomplete CVE-2025-55130 fix)
3841+
// Ensure the symlink target is within allowed permission paths
3842+
if (env->permission()->enabled()) {
3843+
auto target_str = symlink_target_absolute.string();
3844+
auto target_view = std::string_view(target_str);
3845+
if (!env->permission()->is_granted(
3846+
env, permission::PermissionScope::kFileSystemRead, target_view)) {
3847+
return THROW_ERR_ACCESS_DENIED(env,
3848+
"Access to symlink target '%s' denied", target_str.c_str());
3849+
}
3850+
if (!env->permission()->is_granted(
3851+
env, permission::PermissionScope::kFileSystemWrite, target_view)) {
3852+
return THROW_ERR_ACCESS_DENIED(env,
3853+
"Access to symlink target '%s' denied", target_str.c_str());
3854+
}
3855+
}
3856+
38213857
if (dir_entry.is_directory()) {
38223858
std::filesystem::create_directory_symlink(
38233859
symlink_target_absolute, dest_file_path, error);
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// Flags: --experimental-permission --allow-fs-read=* --allow-fs-write=*
2+
'use strict';
3+
4+
const common = require('../common');
5+
6+
if (!common.hasCrypto) common.skip('missing crypto');
7+
8+
const assert = require('assert');
9+
const fs = require('fs');
10+
const path = require('path');
11+
const { execFileSync } = require('child_process');
12+
13+
// This test verifies that fs.cpSync checks symlink target permissions
14+
// when copying directories containing symlinks.
15+
// Regression test for incomplete CVE-2025-55130 fix.
16+
17+
const tmpdir = require('../common/tmpdir');
18+
tmpdir.refresh();
19+
20+
const allowedDir = path.join(tmpdir.path, 'allowed');
21+
const deniedDir = path.join(tmpdir.path, 'denied');
22+
const srcDir = path.join(allowedDir, 'src');
23+
const destDir = path.join(allowedDir, 'dest');
24+
const secretFile = path.join(deniedDir, 'secret.txt');
25+
26+
// Setup directories
27+
fs.mkdirSync(srcDir, { recursive: true });
28+
fs.mkdirSync(destDir, { recursive: true });
29+
fs.mkdirSync(deniedDir, { recursive: true });
30+
fs.writeFileSync(secretFile, 'SECRET_DATA');
31+
32+
// Create symlink pointing outside allowed path
33+
fs.symlinkSync(secretFile, path.join(srcDir, 'link'));
34+
35+
// Run with restricted permissions — only allowedDir is permitted
36+
const result = execFileSync(process.execPath, [
37+
'--experimental-permission',
38+
`--allow-fs-read=${allowedDir}`,
39+
`--allow-fs-write=${allowedDir}`,
40+
'--allow-fs-read=/usr',
41+
'--allow-fs-read=/lib',
42+
'-e',
43+
`
44+
const fs = require('node:fs');
45+
try {
46+
fs.cpSync('${srcDir}/', '${destDir}/', { recursive: true });
47+
console.log('FAIL');
48+
} catch(e) {
49+
console.log(e.code);
50+
}
51+
`,
52+
], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
53+
54+
// cpSync should throw ERR_ACCESS_DENIED because symlink target
55+
// (/tmp/.../denied/secret.txt) is outside allowed paths
56+
assert.strictEqual(
57+
result,
58+
'ERR_ACCESS_DENIED',
59+
`Expected ERR_ACCESS_DENIED but got: ${result}`
60+
);
61+
62+
// Also test verbatimSymlinks path
63+
const destDir2 = path.join(allowedDir, 'dest2');
64+
fs.mkdirSync(destDir2, { recursive: true });
65+
66+
const result2 = execFileSync(process.execPath, [
67+
'--experimental-permission',
68+
`--allow-fs-read=${allowedDir}`,
69+
`--allow-fs-write=${allowedDir}`,
70+
'--allow-fs-read=/usr',
71+
'--allow-fs-read=/lib',
72+
'-e',
73+
`
74+
const fs = require('node:fs');
75+
try {
76+
fs.cpSync('${srcDir}/', '${destDir2}/', {
77+
recursive: true,
78+
verbatimSymlinks: true
79+
});
80+
console.log('FAIL');
81+
} catch(e) {
82+
console.log(e.code);
83+
}
84+
`,
85+
], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
86+
87+
assert.strictEqual(
88+
result2,
89+
'ERR_ACCESS_DENIED',
90+
`verbatimSymlinks: Expected ERR_ACCESS_DENIED but got: ${result2}`
91+
);
92+
93+
console.log('All permission checks passed.');

0 commit comments

Comments
 (0)