diff --git a/node.gyp b/node.gyp index a89b467e84a96f..42e626b2f95398 100644 --- a/node.gyp +++ b/node.gyp @@ -143,6 +143,7 @@ 'src/node_watchdog.cc', 'src/node_worker.cc', 'src/node_zlib.cc', + 'src/path.cc', 'src/permission/child_process_permission.cc', 'src/permission/fs_permission.cc', 'src/permission/inspector_permission.cc', @@ -264,6 +265,7 @@ 'src/node_wasi.h', 'src/node_watchdog.h', 'src/node_worker.h', + 'src/path.h', 'src/permission/child_process_permission.h', 'src/permission/fs_permission.h', 'src/permission/inspector_permission.h', diff --git a/src/env.cc b/src/env.cc index e79bc04b5f0d2b..424f400b8f0f8f 100644 --- a/src/env.cc +++ b/src/env.cc @@ -898,21 +898,25 @@ Environment::Environment(IsolateData* isolate_data, options_->allow_native_addons = false; } flags_ = flags_ | EnvironmentFlags::kNoCreateInspector; - permission()->Apply({"*"}, permission::PermissionScope::kInspector); + permission()->Apply(this, {"*"}, permission::PermissionScope::kInspector); if (!options_->allow_child_process) { - permission()->Apply({"*"}, permission::PermissionScope::kChildProcess); + permission()->Apply( + this, {"*"}, permission::PermissionScope::kChildProcess); } if (!options_->allow_worker_threads) { - permission()->Apply({"*"}, permission::PermissionScope::kWorkerThreads); + permission()->Apply( + this, {"*"}, permission::PermissionScope::kWorkerThreads); } if (!options_->allow_fs_read.empty()) { - permission()->Apply(options_->allow_fs_read, + permission()->Apply(this, + options_->allow_fs_read, permission::PermissionScope::kFileSystemRead); } if (!options_->allow_fs_write.empty()) { - permission()->Apply(options_->allow_fs_write, + permission()->Apply(this, + options_->allow_fs_write, permission::PermissionScope::kFileSystemWrite); } } diff --git a/src/path.cc b/src/path.cc new file mode 100644 index 00000000000000..250faf05bab71e --- /dev/null +++ b/src/path.cc @@ -0,0 +1,272 @@ +#include "path.h" +#include +#include +#include "env-inl.h" +#include "node_internals.h" +#include "util.h" + +namespace node { + +#ifdef _WIN32 +bool IsPathSeparator(const char c) noexcept { + return c == kPathSeparator || c == '/'; +} +#else // POSIX +bool IsPathSeparator(const char c) noexcept { + return c == kPathSeparator; +} +#endif // _WIN32 + +std::string NormalizeString(const std::string_view path, + bool allowAboveRoot, + const std::string_view separator) { + std::string res; + int lastSegmentLength = 0; + int lastSlash = -1; + int dots = 0; + char code; + const auto pathLen = path.size(); + for (uint8_t i = 0; i <= pathLen; ++i) { + if (i < pathLen) { + code = path[i]; + } else if (IsPathSeparator(path[i])) { + break; + } else { + code = node::kPathSeparator; + } + + if (IsPathSeparator(code)) { + if (lastSlash == static_cast(i - 1) || dots == 1) { + // NOOP + } else if (dots == 2) { + int len = res.length(); + if (len < 2 || lastSegmentLength != 2 || res[len - 1] != '.' || + res[len - 2] != '.') { + if (len > 2) { + auto lastSlashIndex = res.find_last_of(separator); + if (lastSlashIndex == std::string::npos) { + res = ""; + lastSegmentLength = 0; + } else { + res = res.substr(0, lastSlashIndex); + len = res.length(); + lastSegmentLength = len - 1 - res.find_last_of(separator); + } + lastSlash = i; + dots = 0; + continue; + } else if (len != 0) { + res = ""; + lastSegmentLength = 0; + lastSlash = i; + dots = 0; + continue; + } + } + + if (allowAboveRoot) { + res += res.length() > 0 ? std::string(separator) + ".." : ".."; + lastSegmentLength = 2; + } + } else { + if (!res.empty()) { + res += std::string(separator) + + std::string(path.substr(lastSlash + 1, i - (lastSlash + 1))); + } else { + res = path.substr(lastSlash + 1, i - (lastSlash + 1)); + } + lastSegmentLength = i - lastSlash - 1; + } + lastSlash = i; + dots = 0; + } else if (code == '.' && dots != -1) { + ++dots; + } else { + dots = -1; + } + } + + return res; +} + +#ifdef _WIN32 +bool IsWindowsDeviceRoot(const char c) noexcept { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); +} + +std::string PathResolve(Environment* env, + const std::vector& paths) { + std::string resolvedDevice = ""; + std::string resolvedTail = ""; + bool resolvedAbsolute = false; + const size_t numArgs = paths.size(); + auto cwd = env->GetCwd(env->exec_path()); + + for (int i = numArgs - 1; i >= -1 && !resolvedAbsolute; i--) { + std::string path; + if (i >= 0) { + path = std::string(paths[i]); + } else if (resolvedDevice.empty()) { + path = cwd; + } else { + // Windows has the concept of drive-specific current working + // directories. If we've resolved a drive letter but not yet an + // absolute path, get cwd for that drive, or the process cwd if + // the drive cwd is not available. We're sure the device is not + // a UNC path at this points, because UNC paths are always absolute. + std::string resolvedDevicePath; + const std::string envvar = "=" + resolvedDevice; + credentials::SafeGetenv(envvar.c_str(), &resolvedDevicePath); + path = resolvedDevicePath.empty() ? cwd : resolvedDevicePath; + + // Verify that a cwd was found and that it actually points + // to our drive. If not, default to the drive's root. + if (path.empty() || + (ToLower(path.substr(0, 2)) != ToLower(resolvedDevice) && + path[2] == '/')) { + path = resolvedDevice + "\\"; + } + } + + const size_t len = path.length(); + int rootEnd = 0; + std::string device = ""; + bool isAbsolute = false; + const char code = path[0]; + + // Try to match a root + if (len == 1) { + if (IsPathSeparator(code)) { + // `path` contains just a path separator + rootEnd = 1; + isAbsolute = true; + } + } else if (IsPathSeparator(code)) { + // Possible UNC root + + // If we started with a separator, we know we at least have an + // absolute path of some kind (UNC or otherwise) + isAbsolute = true; + + if (IsPathSeparator(path[1])) { + // Matched double path separator at beginning + size_t j = 2; + size_t last = j; + // Match 1 or more non-path separators + while (j < len && !IsPathSeparator(path[j])) { + j++; + } + if (j < len && j != last) { + const std::string firstPart = path.substr(last, j - last); + // Matched! + last = j; + // Match 1 or more path separators + while (j < len && IsPathSeparator(path[j])) { + j++; + } + if (j < len && j != last) { + // Matched! + last = j; + // Match 1 or more non-path separators + while (j < len && !IsPathSeparator(path[j])) { + j++; + } + if (j == len || j != last) { + // We matched a UNC root + device = "\\\\" + firstPart + "\\" + path.substr(last, j - last); + rootEnd = j; + } + } + } + } + } else if (IsWindowsDeviceRoot(code) && path[1] == ':') { + // Possible device root + device = path.substr(0, 2); + rootEnd = 2; + if (len > 2 && IsPathSeparator(path[2])) { + // Treat separator following drive name as an absolute path + // indicator + isAbsolute = true; + rootEnd = 3; + } + } + + if (!device.empty()) { + if (!resolvedDevice.empty()) { + if (ToLower(device) != ToLower(resolvedDevice)) { + // This path points to another device so it is not applicable + continue; + } + } else { + resolvedDevice = device; + } + } + + if (resolvedAbsolute) { + if (!resolvedDevice.empty()) { + break; + } + } else { + resolvedTail = path.substr(rootEnd) + "\\" + resolvedTail; + resolvedAbsolute = isAbsolute; + if (isAbsolute && !resolvedDevice.empty()) { + break; + } + } + } + + // At this point the path should be resolved to a full absolute path, + // but handle relative paths to be safe (might happen when process.cwd() + // fails) + + // Normalize the tail path + resolvedTail = NormalizeString(resolvedTail, !resolvedAbsolute, "\\"); + + if (resolvedAbsolute) { + return resolvedDevice + "\\" + resolvedTail; + } + + if (!resolvedDevice.empty() || !resolvedTail.empty()) { + return resolvedDevice + resolvedTail; + } + + return "."; +} +#else // _WIN32 +std::string PathResolve(Environment* env, + const std::vector& paths) { + std::string resolvedPath; + bool resolvedAbsolute = false; + auto cwd = env->GetCwd(env->exec_path()); + const size_t numArgs = paths.size(); + + for (int i = numArgs - 1; i >= -1 && !resolvedAbsolute; i--) { + const std::string& path = + (i >= 0) ? std::string(paths[i]) : env->GetCwd(env->exec_path()); + + if (!path.empty()) { + resolvedPath = std::string(path) + "/" + resolvedPath; + + if (path.front() == '/') { + resolvedAbsolute = true; + break; + } + } + } + + // Normalize the path + auto normalizedPath = NormalizeString(resolvedPath, !resolvedAbsolute, "/"); + + if (resolvedAbsolute) { + return "/" + normalizedPath; + } + + if (normalizedPath.empty()) { + return "."; + } + + return normalizedPath; +} +#endif // _WIN32 + +} // namespace node diff --git a/src/path.h b/src/path.h new file mode 100644 index 00000000000000..532c5f5849652c --- /dev/null +++ b/src/path.h @@ -0,0 +1,25 @@ +#ifndef SRC_PATH_H_ +#define SRC_PATH_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include +#include + +namespace node { + +class Environment; + +bool IsPathSeparator(const char c) noexcept; + +std::string NormalizeString(const std::string_view path, + bool allowAboveRoot, + const std::string_view separator); + +std::string PathResolve(Environment* env, + const std::vector& args); +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#endif // SRC_PATH_H_ diff --git a/src/permission/child_process_permission.cc b/src/permission/child_process_permission.cc index da85083918aa21..43bf5b7cbbc04f 100644 --- a/src/permission/child_process_permission.cc +++ b/src/permission/child_process_permission.cc @@ -9,7 +9,8 @@ namespace permission { // Currently, ChildProcess manage a single state // Once denied, it's always denied -void ChildProcessPermission::Apply(const std::vector& allow, +void ChildProcessPermission::Apply(Environment* env, + const std::vector& allow, PermissionScope scope) { deny_all_ = true; } diff --git a/src/permission/child_process_permission.h b/src/permission/child_process_permission.h index 58f05b01982d6c..844e5ee07509f3 100644 --- a/src/permission/child_process_permission.h +++ b/src/permission/child_process_permission.h @@ -12,7 +12,8 @@ namespace permission { class ChildProcessPermission final : public PermissionBase { public: - void Apply(const std::vector& allow, + void Apply(Environment* env, + const std::vector& allow, PermissionScope scope) override; bool is_granted(PermissionScope perm, const std::string_view& param = "") const override; diff --git a/src/permission/fs_permission.cc b/src/permission/fs_permission.cc index 5780d6cb2607e3..0c881fa266d23d 100644 --- a/src/permission/fs_permission.cc +++ b/src/permission/fs_permission.cc @@ -1,7 +1,7 @@ #include "fs_permission.h" #include "base_object-inl.h" #include "debug_utils-inl.h" -#include "util.h" +#include "path.h" #include "v8.h" #include @@ -117,7 +117,8 @@ namespace permission { // allow = '*' // allow = '/tmp/,/home/example.js' -void FSPermission::Apply(const std::vector& allow, +void FSPermission::Apply(Environment* env, + const std::vector& allow, PermissionScope scope) { for (const std::string& res : allow) { if (res == "*") { @@ -130,7 +131,7 @@ void FSPermission::Apply(const std::vector& allow, } return; } - GrantAccess(scope, res); + GrantAccess(scope, PathResolve(env, {res})); } } diff --git a/src/permission/fs_permission.h b/src/permission/fs_permission.h index 6f35ebc544b04a..d994fd763aaf14 100644 --- a/src/permission/fs_permission.h +++ b/src/permission/fs_permission.h @@ -15,7 +15,8 @@ namespace permission { class FSPermission final : public PermissionBase { public: - void Apply(const std::vector& allow, + void Apply(Environment* env, + const std::vector& allow, PermissionScope scope) override; bool is_granted(PermissionScope perm, const std::string_view& param) const override; diff --git a/src/permission/inspector_permission.cc b/src/permission/inspector_permission.cc index 34e55db4bef590..59d8547b375222 100644 --- a/src/permission/inspector_permission.cc +++ b/src/permission/inspector_permission.cc @@ -8,7 +8,8 @@ namespace permission { // Currently, Inspector manage a single state // Once denied, it's always denied -void InspectorPermission::Apply(const std::vector& allow, +void InspectorPermission::Apply(Environment* env, + const std::vector& allow, PermissionScope scope) { deny_all_ = true; } diff --git a/src/permission/inspector_permission.h b/src/permission/inspector_permission.h index fc8b357c4fcb15..2b32ad0b078d5d 100644 --- a/src/permission/inspector_permission.h +++ b/src/permission/inspector_permission.h @@ -12,7 +12,8 @@ namespace permission { class InspectorPermission final : public PermissionBase { public: - void Apply(const std::vector& allow, + void Apply(Environment* env, + const std::vector& allow, PermissionScope scope) override; bool is_granted(PermissionScope perm, const std::string_view& param = "") const override; diff --git a/src/permission/permission.cc b/src/permission/permission.cc index 4392f49b66e9b7..84235ed1b57731 100644 --- a/src/permission/permission.cc +++ b/src/permission/permission.cc @@ -130,11 +130,12 @@ void Permission::EnablePermissions() { } } -void Permission::Apply(const std::vector& allow, +void Permission::Apply(Environment* env, + const std::vector& allow, PermissionScope scope) { auto permission = nodes_.find(scope); if (permission != nodes_.end()) { - permission->second->Apply(allow, scope); + permission->second->Apply(env, allow, scope); } } diff --git a/src/permission/permission.h b/src/permission/permission.h index 942937a80cae28..3d37169eaaa17e 100644 --- a/src/permission/permission.h +++ b/src/permission/permission.h @@ -49,7 +49,9 @@ class Permission { const std::string_view& res); // CLI Call - void Apply(const std::vector& allow, PermissionScope scope); + void Apply(Environment* env, + const std::vector& allow, + PermissionScope scope); void EnablePermissions(); private: diff --git a/src/permission/permission_base.h b/src/permission/permission_base.h index b817efb88a76bb..429a1fa854e2f1 100644 --- a/src/permission/permission_base.h +++ b/src/permission/permission_base.h @@ -10,6 +10,8 @@ namespace node { +class Environment; + namespace permission { #define FILESYSTEM_PERMISSIONS(V) \ @@ -39,7 +41,8 @@ enum class PermissionScope { class PermissionBase { public: - virtual void Apply(const std::vector& allow, + virtual void Apply(Environment* env, + const std::vector& allow, PermissionScope scope) = 0; virtual bool is_granted(PermissionScope perm, const std::string_view& param = "") const = 0; diff --git a/src/permission/worker_permission.cc b/src/permission/worker_permission.cc index 78293589faa3bd..1eba2eab09bee1 100644 --- a/src/permission/worker_permission.cc +++ b/src/permission/worker_permission.cc @@ -9,7 +9,8 @@ namespace permission { // Currently, PolicyDenyWorker manage a single state // Once denied, it's always denied -void WorkerPermission::Apply(const std::vector& allow, +void WorkerPermission::Apply(Environment* env, + const std::vector& allow, PermissionScope scope) { deny_all_ = true; } diff --git a/src/permission/worker_permission.h b/src/permission/worker_permission.h index 99d6b039ee22c1..29b6a523122c6e 100644 --- a/src/permission/worker_permission.h +++ b/src/permission/worker_permission.h @@ -12,7 +12,8 @@ namespace permission { class WorkerPermission final : public PermissionBase { public: - void Apply(const std::vector& allow, + void Apply(Environment* env, + const std::vector& allow, PermissionScope scope) override; bool is_granted(PermissionScope perm, const std::string_view& param = "") const override; diff --git a/test/cctest/test_path.cc b/test/cctest/test_path.cc new file mode 100644 index 00000000000000..44f3b30ddff341 --- /dev/null +++ b/test/cctest/test_path.cc @@ -0,0 +1,47 @@ +#include "debug_utils-inl.h" +#include "env-inl.h" +#include "gtest/gtest.h" +#include "node_options.h" +#include "node_test_fixture.h" +#include "path.h" + +using node::PathResolve; + +class PathTest : public EnvironmentTestFixture {}; + +TEST_F(PathTest, PathResolve) { + const v8::HandleScope handle_scope(isolate_); + Argv argv; + Env env{handle_scope, argv, node::EnvironmentFlags::kNoBrowserGlobals}; + auto cwd = (*env)->GetCwd((*env)->exec_path()); +#ifdef _WIN32 + EXPECT_EQ(PathResolve(*env, {"c:/blah\\blah", "d:/games", "c:../a"}), + "c:\\blah\\a"); + EXPECT_EQ(PathResolve(*env, {"c:/ignore", "d:\\a/b\\c/d", "\\e.exe"}), + "d:\\e.exe"); + EXPECT_EQ(PathResolve(*env, {"c:/ignore", "c:/some/file"}), "c:\\some\\file"); + EXPECT_EQ(PathResolve(*env, {"d:/ignore", "d:some/dir//"}), + "d:\\ignore\\some\\dir"); + EXPECT_EQ(PathResolve(*env, {"."}), cwd); + EXPECT_EQ(PathResolve(*env, {"//server/share", "..", "relative\\"}), + "\\\\server\\share\\relative"); + EXPECT_EQ(PathResolve(*env, {"c:/", "//"}), "c:\\"); + EXPECT_EQ(PathResolve(*env, {"c:/", "//dir"}), "c:\\dir"); + EXPECT_EQ(PathResolve(*env, {"c:/", "//server/share"}), + "\\\\server\\share\\"); + EXPECT_EQ(PathResolve(*env, {"c:/", "//server//share"}), + "\\\\server\\share\\"); + EXPECT_EQ(PathResolve(*env, {"c:/", "///some//dir"}), "c:\\some\\dir"); + EXPECT_EQ( + PathResolve(*env, {"C:\\foo\\tmp.3\\", "..\\tmp.3\\cycles\\root.js"}), + "C:\\foo\\tmp.3\\cycles\\root.js"); +#else + EXPECT_EQ(PathResolve(*env, {"/var/lib", "../", "file/"}), "/var/file"); + EXPECT_EQ(PathResolve(*env, {"/var/lib", "/../", "file/"}), "/file"); + EXPECT_EQ(PathResolve(*env, {"a/b/c/", "../../.."}), cwd); + EXPECT_EQ(PathResolve(*env, {"."}), cwd); + EXPECT_EQ(PathResolve(*env, {"/some/dir", ".", "/absolute/"}), "/absolute"); + EXPECT_EQ(PathResolve(*env, {"/foo/tmp.3/", "../tmp.3/cycles/root.js"}), + "/foo/tmp.3/cycles/root.js"); +#endif +} diff --git a/test/parallel/test-permission-fs-absolute-path.js b/test/parallel/test-permission-fs-absolute-path.js new file mode 100644 index 00000000000000..b7897743941d2e --- /dev/null +++ b/test/parallel/test-permission-fs-absolute-path.js @@ -0,0 +1,31 @@ +// Flags: --experimental-permission --allow-fs-read=* --allow-child-process +'use strict'; + +const common = require('../common'); +const path = require('path'); +common.skipIfWorker(); + +const assert = require('assert'); +const { spawnSync } = require('child_process'); + +{ + // Relative path as CLI args are supported + const { status, stdout } = spawnSync( + process.execPath, + [ + '--experimental-permission', + '--allow-fs-read', '*', + '--allow-fs-write', path.resolve('../fixtures/permission/deny/regular-file.md'), + '-e', + ` + const path = require("path"); + const absolutePath = path.resolve("../fixtures/permission/deny/regular-file.md"); + console.log(process.permission.has("fs.write", absolutePath)); + `, + ] + ); + + const [fsWrite] = stdout.toString().split('\n'); + assert.strictEqual(fsWrite, 'true'); + assert.strictEqual(status, 0); +} diff --git a/test/parallel/test-permission-fs-relative-path.js b/test/parallel/test-permission-fs-relative-path.js index 74b4095e86663a..628e9918660088 100644 --- a/test/parallel/test-permission-fs-relative-path.js +++ b/test/parallel/test-permission-fs-relative-path.js @@ -8,7 +8,7 @@ const assert = require('assert'); const { spawnSync } = require('child_process'); { - // Relative path as CLI args are NOT supported yet + // Relative path as CLI args are supported const { status, stdout } = spawnSync( process.execPath, [ @@ -25,6 +25,6 @@ const { spawnSync } = require('child_process'); ); const [fsWrite] = stdout.toString().split('\n'); - assert.strictEqual(fsWrite, 'false'); + assert.strictEqual(fsWrite, 'true'); assert.strictEqual(status, 0); }