Skip to content

Commit 7a2f106

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 Signed-off-by: fg0x0 <fg0x0@local>
1 parent bbf51ad commit 7a2f106

2 files changed

Lines changed: 144 additions & 1 deletion

File tree

src/node_file.cc

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ using v8::Undefined;
8888
using v8::Value;
8989

9090
#ifndef S_ISDIR
91-
#define S_ISDIR(mode) (((mode)&S_IFMT) == S_IFDIR)
91+
#define S_ISDIR(mode) (((mode) & S_IFMT) == S_IFDIR)
9292
#endif
9393

9494
#ifdef __POSIX__
@@ -3752,6 +3752,30 @@ static void CpSyncCopyDir(const FunctionCallbackInfo<Value>& args) {
37523752

37533753
if (dir_entry.is_symlink()) {
37543754
if (verbatim_symlinks) {
3755+
// Permission check for verbatimSymlinks path (incomplete
3756+
// 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,
3766+
permission::PermissionScope::kFileSystemRead,
3767+
verb_view) ||
3768+
!env->permission()->is_granted(
3769+
env,
3770+
permission::PermissionScope::kFileSystemWrite,
3771+
verb_view)) {
3772+
return THROW_ERR_ACCESS_DENIED(
3773+
env,
3774+
"Access to symlink target '%s' denied",
3775+
verb_str.c_str());
3776+
}
3777+
}
3778+
37553779
std::filesystem::copy_symlink(
37563780
dir_entry.path(), dest_file_path, error);
37573781
if (error) {
@@ -3818,6 +3842,32 @@ static void CpSyncCopyDir(const FunctionCallbackInfo<Value>& args) {
38183842
}
38193843
auto symlink_target_absolute = std::filesystem::weakly_canonical(
38203844
std::filesystem::absolute(src / symlink_target));
3845+
3846+
// Permission check for symlink target (incomplete CVE-2025-55130 fix)
3847+
// Ensure the symlink target is within allowed permission paths
3848+
if (env->permission()->enabled()) {
3849+
auto target_str = symlink_target_absolute.string();
3850+
auto target_view = std::string_view(target_str);
3851+
if (!env->permission()->is_granted(
3852+
env,
3853+
permission::PermissionScope::kFileSystemRead,
3854+
target_view)) {
3855+
return THROW_ERR_ACCESS_DENIED(
3856+
env,
3857+
"Access to symlink target '%s' denied",
3858+
target_str.c_str());
3859+
}
3860+
if (!env->permission()->is_granted(
3861+
env,
3862+
permission::PermissionScope::kFileSystemWrite,
3863+
target_view)) {
3864+
return THROW_ERR_ACCESS_DENIED(
3865+
env,
3866+
"Access to symlink target '%s' denied",
3867+
target_str.c_str());
3868+
}
3869+
}
3870+
38213871
if (dir_entry.is_directory()) {
38223872
std::filesystem::create_directory_symlink(
38233873
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)