Skip to content

Commit

Permalink
src,permission: resolve path on fs_permission
Browse files Browse the repository at this point in the history
Signed-off-by: RafaelGSS <[email protected]>
PR-URL: #52761
Reviewed-By: Marco Ippolito <[email protected]>
Reviewed-By: Rich Trott <[email protected]>
  • Loading branch information
RafaelGSS authored May 3, 2024
1 parent a480e52 commit 15456e4
Show file tree
Hide file tree
Showing 21 changed files with 68 additions and 131 deletions.
2 changes: 0 additions & 2 deletions doc/api/permissions.md
Original file line number Diff line number Diff line change
Expand Up @@ -583,8 +583,6 @@ There are constraints you need to know before using this system:

#### Limitations and Known Issues

* When the permission model is enabled, Node.js may resolve some paths
differently than when it is disabled.
* Symbolic links will be followed even to locations outside of the set of paths
that access has been granted to. Relative symbolic links may allow access to
arbitrary files and directories. When starting applications with the
Expand Down
29 changes: 1 addition & 28 deletions lib/internal/fs/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,8 @@ const {
Symbol,
TypedArrayPrototypeAt,
TypedArrayPrototypeIncludes,
uncurryThis,
} = primordials;

const permission = require('internal/process/permission');

const { Buffer } = require('buffer');
const {
UVException,
Expand Down Expand Up @@ -68,8 +65,6 @@ const kType = Symbol('type');
const kStats = Symbol('stats');
const assert = require('internal/assert');

const { encodeUtf8String } = internalBinding('encoding_binding');

const {
fs: {
F_OK = 0,
Expand Down Expand Up @@ -736,31 +731,10 @@ const validatePath = hideStackFrames((path, propName = 'path') => {
);
});

// TODO(rafaelgss): implement the path.resolve on C++ side
// See: https://github.com/nodejs/node/pull/44004#discussion_r930958420
// The permission model needs the absolute path for the fs_permission
const resolvePath = pathModule.resolve;
const { isBuffer: BufferIsBuffer, from: BufferFrom } = Buffer;
const BufferToString = uncurryThis(Buffer.prototype.toString);
function possiblyTransformPath(path) {
if (permission.isEnabled()) {
if (typeof path === 'string') {
return resolvePath(path);
}
assert(isUint8Array(path));
if (!BufferIsBuffer(path)) path = BufferFrom(path);
// Avoid Buffer.from() and use a C++ binding instead to encode the result
// of path.resolve() in order to prevent path traversal attacks that
// monkey-patch Buffer internals.
return encodeUtf8String(resolvePath(BufferToString(path)));
}
return path;
}

const getValidatedPath = hideStackFrames((fileURLOrPath, propName = 'path') => {
const path = toPathIfFileURL(fileURLOrPath);
validatePath(path, propName);
return possiblyTransformPath(path);
return path;
});

const getValidatedFd = hideStackFrames((fd, propName = 'fd') => {
Expand Down Expand Up @@ -994,7 +968,6 @@ module.exports = {
getValidatedFd,
getValidatedPath,
handleErrorFromBinding,
possiblyTransformPath,
preprocessSymlinkDestination,
realpathCacheKey: Symbol('realpathCacheKey'),
getStatFsFromBinding,
Expand Down
4 changes: 2 additions & 2 deletions src/compile_cache.cc
Original file line number Diff line number Diff line change
Expand Up @@ -354,15 +354,15 @@ bool CompileCacheHandler::InitializeDirectory(Environment* env,
cache_dir);

if (UNLIKELY(!env->permission()->is_granted(
permission::PermissionScope::kFileSystemWrite, cache_dir))) {
env, permission::PermissionScope::kFileSystemWrite, cache_dir))) {
Debug("[compile cache] skipping cache because write permission for %s "
"is not granted\n",
cache_dir);
return false;
}

if (UNLIKELY(!env->permission()->is_granted(
permission::PermissionScope::kFileSystemRead, cache_dir))) {
env, permission::PermissionScope::kFileSystemRead, cache_dir))) {
Debug("[compile cache] skipping cache because read permission for %s "
"is not granted\n",
cache_dir);
Expand Down
1 change: 1 addition & 0 deletions src/node_modules.cc
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,7 @@ const BindingData::PackageConfig* BindingData::TraverseParent(
// to walk upwards
if (UNLIKELY(is_permissions_enabled &&
!env->permission()->is_granted(
env,
permission::PermissionScope::kFileSystemRead,
std::string(check_path) + kPathSeparator))) {
return nullptr;
Expand Down
2 changes: 1 addition & 1 deletion src/node_worker.cc
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ Worker::Worker(Environment* env,
// Without this check, to use the permission model with
// workers (--allow-worker) one would need to pass --allow-inspector as well
if (env->permission()->is_granted(
node::permission::PermissionScope::kInspector)) {
env, node::permission::PermissionScope::kInspector)) {
inspector_parent_handle_ =
GetInspectorParentHandle(env, thread_id_, url.c_str(), name.c_str());
}
Expand Down
3 changes: 2 additions & 1 deletion src/permission/child_process_permission.cc
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ void ChildProcessPermission::Apply(Environment* env,
deny_all_ = true;
}

bool ChildProcessPermission::is_granted(PermissionScope perm,
bool ChildProcessPermission::is_granted(Environment* env,
PermissionScope perm,
const std::string_view& param) const {
return deny_all_ == false;
}
Expand Down
3 changes: 2 additions & 1 deletion src/permission/child_process_permission.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ class ChildProcessPermission final : public PermissionBase {
void Apply(Environment* env,
const std::vector<std::string>& allow,
PermissionScope scope) override;
bool is_granted(PermissionScope perm,
bool is_granted(Environment* env,
PermissionScope perm,
const std::string_view& param = "") const override;

private:
Expand Down
16 changes: 10 additions & 6 deletions src/permission/fs_permission.cc
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include "fs_permission.h"
#include "base_object-inl.h"
#include "debug_utils-inl.h"
#include "env.h"
#include "path.h"
#include "v8.h"

Expand Down Expand Up @@ -51,21 +52,23 @@ void FreeRecursivelyNode(
}

bool is_tree_granted(
node::Environment* env,
const node::permission::FSPermission::RadixTree* granted_tree,
const std::string_view& param) {
std::string resolved_param = node::PathResolve(env, {param});
#ifdef _WIN32
// is UNC file path
if (param.rfind("\\\\", 0) == 0) {
if (resolved_param.rfind("\\\\", 0) == 0) {
// return lookup with normalized param
size_t starting_pos = 4; // "\\?\"
if (param.rfind("\\\\?\\UNC\\") == 0) {
if (resolved_param.rfind("\\\\?\\UNC\\") == 0) {
starting_pos += 4; // "UNC\"
}
auto normalized = param.substr(starting_pos);
return granted_tree->Lookup(normalized, true);
}
#endif
return granted_tree->Lookup(param, true);
return granted_tree->Lookup(resolved_param, true);
}

void PrintTree(const node::permission::FSPermission::RadixTree::Node* node,
Expand Down Expand Up @@ -146,19 +149,20 @@ void FSPermission::GrantAccess(PermissionScope perm, const std::string& res) {
}
}

bool FSPermission::is_granted(PermissionScope perm,
bool FSPermission::is_granted(Environment* env,
PermissionScope perm,
const std::string_view& param = "") const {
switch (perm) {
case PermissionScope::kFileSystem:
return allow_all_in_ && allow_all_out_;
case PermissionScope::kFileSystemRead:
return !deny_all_in_ &&
((param.empty() && allow_all_in_) || allow_all_in_ ||
is_tree_granted(&granted_in_fs_, param));
is_tree_granted(env, &granted_in_fs_, param));
case PermissionScope::kFileSystemWrite:
return !deny_all_out_ &&
((param.empty() && allow_all_out_) || allow_all_out_ ||
is_tree_granted(&granted_out_fs_, param));
is_tree_granted(env, &granted_out_fs_, param));
default:
return false;
}
Expand Down
3 changes: 2 additions & 1 deletion src/permission/fs_permission.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ class FSPermission final : public PermissionBase {
void Apply(Environment* env,
const std::vector<std::string>& allow,
PermissionScope scope) override;
bool is_granted(PermissionScope perm,
bool is_granted(Environment* env,
PermissionScope perm,
const std::string_view& param) const override;

struct RadixTree {
Expand Down
3 changes: 2 additions & 1 deletion src/permission/inspector_permission.cc
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ void InspectorPermission::Apply(Environment* env,
deny_all_ = true;
}

bool InspectorPermission::is_granted(PermissionScope perm,
bool InspectorPermission::is_granted(Environment* env,
PermissionScope perm,
const std::string_view& param) const {
return deny_all_ == false;
}
Expand Down
3 changes: 2 additions & 1 deletion src/permission/inspector_permission.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ class InspectorPermission final : public PermissionBase {
void Apply(Environment* env,
const std::vector<std::string>& allow,
PermissionScope scope) override;
bool is_granted(PermissionScope perm,
bool is_granted(Environment* env,
PermissionScope perm,
const std::string_view& param = "") const override;

private:
Expand Down
4 changes: 2 additions & 2 deletions src/permission/permission.cc
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,10 @@ static void Has(const FunctionCallbackInfo<Value>& args) {
return;
}
return args.GetReturnValue().Set(
env->permission()->is_granted(scope, *utf8_arg));
env->permission()->is_granted(env, scope, *utf8_arg));
}

return args.GetReturnValue().Set(env->permission()->is_granted(scope));
return args.GetReturnValue().Set(env->permission()->is_granted(env, scope));
}

} // namespace
Expand Down
14 changes: 8 additions & 6 deletions src/permission/permission.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ namespace permission {

#define THROW_IF_INSUFFICIENT_PERMISSIONS(env, perm_, resource_, ...) \
do { \
if (UNLIKELY(!(env)->permission()->is_granted(perm_, resource_))) { \
if (UNLIKELY(!(env)->permission()->is_granted(env, perm_, resource_))) { \
node::permission::Permission::ThrowAccessDenied( \
(env), perm_, resource_); \
return __VA_ARGS__; \
Expand All @@ -37,7 +37,7 @@ namespace permission {
#define ASYNC_THROW_IF_INSUFFICIENT_PERMISSIONS( \
env, wrap, perm_, resource_, ...) \
do { \
if (UNLIKELY(!(env)->permission()->is_granted(perm_, resource_))) { \
if (UNLIKELY(!(env)->permission()->is_granted(env, perm_, resource_))) { \
node::permission::Permission::AsyncThrowAccessDenied( \
(env), wrap, perm_, resource_); \
return __VA_ARGS__; \
Expand All @@ -48,10 +48,11 @@ class Permission {
public:
Permission();

FORCE_INLINE bool is_granted(const PermissionScope permission,
FORCE_INLINE bool is_granted(Environment* env,
const PermissionScope permission,
const std::string_view& res = "") const {
if (LIKELY(!enabled_)) return true;
return is_scope_granted(permission, res);
return is_scope_granted(env, permission, res);
}

FORCE_INLINE bool enabled() const { return enabled_; }
Expand All @@ -73,11 +74,12 @@ class Permission {
void EnablePermissions();

private:
COLD_NOINLINE bool is_scope_granted(const PermissionScope permission,
COLD_NOINLINE bool is_scope_granted(Environment* env,
const PermissionScope permission,
const std::string_view& res = "") const {
auto perm_node = nodes_.find(permission);
if (perm_node != nodes_.end()) {
return perm_node->second->is_granted(permission, res);
return perm_node->second->is_granted(env, permission, res);
}
return false;
}
Expand Down
3 changes: 2 additions & 1 deletion src/permission/permission_base.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ class PermissionBase {
virtual void Apply(Environment* env,
const std::vector<std::string>& allow,
PermissionScope scope) = 0;
virtual bool is_granted(PermissionScope perm,
virtual bool is_granted(Environment* env,
PermissionScope perm,
const std::string_view& param = "") const = 0;
};

Expand Down
3 changes: 2 additions & 1 deletion src/permission/worker_permission.cc
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ void WorkerPermission::Apply(Environment* env,
deny_all_ = true;
}

bool WorkerPermission::is_granted(PermissionScope perm,
bool WorkerPermission::is_granted(Environment* env,
PermissionScope perm,
const std::string_view& param) const {
return deny_all_ == false;
}
Expand Down
3 changes: 2 additions & 1 deletion src/permission/worker_permission.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ class WorkerPermission final : public PermissionBase {
void Apply(Environment* env,
const std::vector<std::string>& allow,
PermissionScope scope) override;
bool is_granted(PermissionScope perm,
bool is_granted(Environment* env,
PermissionScope perm,
const std::string_view& param = "") const override;

private:
Expand Down
32 changes: 16 additions & 16 deletions test/fixtures/permission/fs-traversal.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,49 +31,49 @@ const uint8ArrayTraversalPath = new TextEncoder().encode(traversalPath);
fs.writeFile(traversalPath, 'test', common.expectsError({
code: 'ERR_ACCESS_DENIED',
permission: 'FileSystemWrite',
resource: path.toNamespacedPath(resolve(traversalPath)),
resource: path.toNamespacedPath(traversalPath),
}));
}

{
fs.readFile(traversalPath, common.expectsError({
code: 'ERR_ACCESS_DENIED',
permission: 'FileSystemRead',
resource: path.toNamespacedPath(resolve(traversalPath)),
resource: path.toNamespacedPath(traversalPath),
}));
}

{
assert.throws(() => {
fs.mkdtempSync(traversalFolderPath)
}, {
fs.mkdtempSync(traversalFolderPath);
}, common.expectsError({
code: 'ERR_ACCESS_DENIED',
permission: 'FileSystemWrite',
resource: resolve(traversalFolderPath + 'XXXXXX'),
});
resource: traversalFolderPath + 'XXXXXX',
}));
}

{
fs.mkdtemp(traversalFolderPath, common.expectsError({
code: 'ERR_ACCESS_DENIED',
permission: 'FileSystemWrite',
resource: resolve(traversalFolderPath + 'XXXXXX'),
}));
fs.mkdtemp(traversalFolderPath, common.expectsError({
code: 'ERR_ACCESS_DENIED',
permission: 'FileSystemWrite',
resource: traversalFolderPath + 'XXXXXX',
}));
}

{
fs.readFile(bufferTraversalPath, common.expectsError({
code: 'ERR_ACCESS_DENIED',
permission: 'FileSystemRead',
resource: resolve(traversalPath),
resource: traversalPath,
}));
}

{
fs.readFile(uint8ArrayTraversalPath, common.expectsError({
code: 'ERR_ACCESS_DENIED',
permission: 'FileSystemRead',
resource: resolve(traversalPath),
resource: traversalPath,
}));
}

Expand All @@ -93,7 +93,7 @@ const uint8ArrayTraversalPath = new TextEncoder().encode(traversalPath);
}, common.expectsError({
code: 'ERR_ACCESS_DENIED',
permission: 'FileSystemRead',
resource: resolve(cwd.toString()),
resource: cwd.toString(),
}));
}

Expand All @@ -118,15 +118,15 @@ const uint8ArrayTraversalPath = new TextEncoder().encode(traversalPath);
}, common.expectsError({
code: 'ERR_ACCESS_DENIED',
permission: 'FileSystemRead',
resource: resolve(traversalPathWithExtraChars),
resource: traversalPathWithExtraChars,
}));

assert.throws(() => {
fs.readFileSync(new TextEncoder().encode(traversalPathWithExtraBytes.toString()));
}, common.expectsError({
code: 'ERR_ACCESS_DENIED',
permission: 'FileSystemRead',
resource: resolve(traversalPathWithExtraChars),
resource: traversalPathWithExtraChars,
}));
}

Expand Down
Loading

0 comments on commit 15456e4

Please sign in to comment.