-
-
Notifications
You must be signed in to change notification settings - Fork 35.5k
permission: check symlink target in cpSync #63208
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -88,7 +88,7 @@ using v8::Undefined; | |||||||
| using v8::Value; | ||||||||
|
|
||||||||
| #ifndef S_ISDIR | ||||||||
| #define S_ISDIR(mode) (((mode)&S_IFMT) == S_IFDIR) | ||||||||
| #define S_ISDIR(mode) (((mode) & S_IFMT) == S_IFDIR) | ||||||||
| #endif | ||||||||
|
|
||||||||
| #ifdef __POSIX__ | ||||||||
|
|
@@ -3752,6 +3752,30 @@ static void CpSyncCopyDir(const FunctionCallbackInfo<Value>& args) { | |||||||
|
|
||||||||
| if (dir_entry.is_symlink()) { | ||||||||
| if (verbatim_symlinks) { | ||||||||
| // Permission check for verbatimSymlinks path (incomplete | ||||||||
| // CVE-2025-55130 fix) | ||||||||
|
Comment on lines
+3755
to
+3756
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
It's not an incomplete fix, it's not a vulnerability, I told you. |
||||||||
| if (env->permission()->enabled()) { | ||||||||
| auto verb_target = std::filesystem::read_symlink(src, error); | ||||||||
| if (error) break; | ||||||||
| auto verb_target_abs = std::filesystem::weakly_canonical( | ||||||||
| std::filesystem::absolute(src.parent_path() / verb_target)); | ||||||||
| auto verb_str = verb_target_abs.string(); | ||||||||
| auto verb_view = std::string_view(verb_str); | ||||||||
|
Comment on lines
+3758
to
+3763
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We shouldn't resolve the path and then perform the check, this goes against permission model behavior, we never call a fs syscall to check if we can in-fact read/write to a file, this is controversial (that's why symlinks aren't supported) |
||||||||
| if (!env->permission()->is_granted( | ||||||||
| env, | ||||||||
| permission::PermissionScope::kFileSystemRead, | ||||||||
| verb_view) || | ||||||||
| !env->permission()->is_granted( | ||||||||
| env, | ||||||||
| permission::PermissionScope::kFileSystemWrite, | ||||||||
| verb_view)) { | ||||||||
| return THROW_ERR_ACCESS_DENIED( | ||||||||
| env, | ||||||||
| "Access to symlink target '%s' denied", | ||||||||
| verb_str.c_str()); | ||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
| std::filesystem::copy_symlink( | ||||||||
| dir_entry.path(), dest_file_path, error); | ||||||||
| if (error) { | ||||||||
|
|
@@ -3818,6 +3842,32 @@ static void CpSyncCopyDir(const FunctionCallbackInfo<Value>& args) { | |||||||
| } | ||||||||
| auto symlink_target_absolute = std::filesystem::weakly_canonical( | ||||||||
| std::filesystem::absolute(src / symlink_target)); | ||||||||
|
|
||||||||
| // Permission check for symlink target (incomplete CVE-2025-55130 fix) | ||||||||
| // Ensure the symlink target is within allowed permission paths | ||||||||
| if (env->permission()->enabled()) { | ||||||||
| auto target_str = symlink_target_absolute.string(); | ||||||||
| auto target_view = std::string_view(target_str); | ||||||||
| if (!env->permission()->is_granted( | ||||||||
| env, | ||||||||
| permission::PermissionScope::kFileSystemRead, | ||||||||
| target_view)) { | ||||||||
| return THROW_ERR_ACCESS_DENIED( | ||||||||
| env, | ||||||||
| "Access to symlink target '%s' denied", | ||||||||
| target_str.c_str()); | ||||||||
| } | ||||||||
| if (!env->permission()->is_granted( | ||||||||
| env, | ||||||||
| permission::PermissionScope::kFileSystemWrite, | ||||||||
| target_view)) { | ||||||||
| return THROW_ERR_ACCESS_DENIED( | ||||||||
| env, | ||||||||
| "Access to symlink target '%s' denied", | ||||||||
| target_str.c_str()); | ||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
| if (dir_entry.is_directory()) { | ||||||||
| std::filesystem::create_directory_symlink( | ||||||||
| symlink_target_absolute, dest_file_path, error); | ||||||||
|
|
||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| // Flags: --experimental-permission --allow-fs-read=* --allow-fs-write=* | ||
| 'use strict'; | ||
|
|
||
| const common = require('../common'); | ||
|
|
||
| if (!common.hasCrypto) common.skip('missing crypto'); | ||
|
|
||
| const assert = require('assert'); | ||
| const fs = require('fs'); | ||
| const path = require('path'); | ||
| const { execFileSync } = require('child_process'); | ||
|
|
||
| // This test verifies that fs.cpSync checks symlink target permissions | ||
| // when copying directories containing symlinks. | ||
| // Regression test for incomplete CVE-2025-55130 fix. | ||
|
|
||
| const tmpdir = require('../common/tmpdir'); | ||
| tmpdir.refresh(); | ||
|
|
||
| const allowedDir = path.join(tmpdir.path, 'allowed'); | ||
| const deniedDir = path.join(tmpdir.path, 'denied'); | ||
| const srcDir = path.join(allowedDir, 'src'); | ||
| const destDir = path.join(allowedDir, 'dest'); | ||
| const secretFile = path.join(deniedDir, 'secret.txt'); | ||
|
|
||
| // Setup directories | ||
| fs.mkdirSync(srcDir, { recursive: true }); | ||
| fs.mkdirSync(destDir, { recursive: true }); | ||
| fs.mkdirSync(deniedDir, { recursive: true }); | ||
| fs.writeFileSync(secretFile, 'SECRET_DATA'); | ||
|
|
||
| // Create symlink pointing outside allowed path | ||
| fs.symlinkSync(secretFile, path.join(srcDir, 'link')); | ||
|
|
||
| // Run with restricted permissions — only allowedDir is permitted | ||
| const result = execFileSync(process.execPath, [ | ||
| '--experimental-permission', | ||
| `--allow-fs-read=${allowedDir}`, | ||
| `--allow-fs-write=${allowedDir}`, | ||
| '--allow-fs-read=/usr', | ||
| '--allow-fs-read=/lib', | ||
| '-e', | ||
| ` | ||
| const fs = require('node:fs'); | ||
| try { | ||
| fs.cpSync('${srcDir}/', '${destDir}/', { recursive: true }); | ||
| console.log('FAIL'); | ||
| } catch(e) { | ||
| console.log(e.code); | ||
| } | ||
| `, | ||
| ], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim(); | ||
|
|
||
| // cpSync should throw ERR_ACCESS_DENIED because symlink target | ||
| // (/tmp/.../denied/secret.txt) is outside allowed paths | ||
| assert.strictEqual( | ||
| result, | ||
| 'ERR_ACCESS_DENIED', | ||
| `Expected ERR_ACCESS_DENIED but got: ${result}` | ||
| ); | ||
|
|
||
| // Also test verbatimSymlinks path | ||
| const destDir2 = path.join(allowedDir, 'dest2'); | ||
| fs.mkdirSync(destDir2, { recursive: true }); | ||
|
|
||
| const result2 = execFileSync(process.execPath, [ | ||
| '--experimental-permission', | ||
| `--allow-fs-read=${allowedDir}`, | ||
| `--allow-fs-write=${allowedDir}`, | ||
| '--allow-fs-read=/usr', | ||
| '--allow-fs-read=/lib', | ||
| '-e', | ||
| ` | ||
| const fs = require('node:fs'); | ||
| try { | ||
| fs.cpSync('${srcDir}/', '${destDir2}/', { | ||
| recursive: true, | ||
| verbatimSymlinks: true | ||
| }); | ||
| console.log('FAIL'); | ||
| } catch(e) { | ||
| console.log(e.code); | ||
| } | ||
| `, | ||
| ], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim(); | ||
|
|
||
| assert.strictEqual( | ||
| result2, | ||
| 'ERR_ACCESS_DENIED', | ||
| `verbatimSymlinks: Expected ERR_ACCESS_DENIED but got: ${result2}` | ||
| ); | ||
|
|
||
| console.log('All permission checks passed.'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this change is not required