Skip to content

Commit b2e3d74

Browse files
committed
permission: fix some vulnerabilities in fs
Without this patch, any restrictions imposed by the permission model can be easily bypassed, granting full read and write access to any file. On Windows, this could even be used to delete files that are supposed to be write-protected. Fixes: nodejs#47090
1 parent 334bb17 commit b2e3d74

File tree

3 files changed

+71
-34
lines changed

3 files changed

+71
-34
lines changed

src/node_file.cc

+27-34
Original file line numberDiff line numberDiff line change
@@ -1949,6 +1949,31 @@ static void ReadDir(const FunctionCallbackInfo<Value>& args) {
19491949
}
19501950
}
19511951

1952+
static inline bool CheckOpenPermissions(Environment* env,
1953+
const BufferValue& path,
1954+
int flags) {
1955+
// These flags capture the intention of the open() call.
1956+
const int rwflags = flags & (UV_FS_O_RDONLY | UV_FS_O_WRONLY | UV_FS_O_RDWR);
1957+
1958+
// These flags have write-like side effects even with O_RDONLY, at least on
1959+
// some operating systems. On Windows, for example, O_RDONLY | O_TEMPORARY
1960+
// can be used to delete a file. Bizarre.
1961+
const int write_as_side_effect = flags & (UV_FS_O_APPEND | UV_FS_O_CREAT |
1962+
UV_FS_O_TRUNC | UV_FS_O_TEMPORARY);
1963+
1964+
// TODO(rafaelgss): it can be optimized to avoid two permission checks
1965+
auto pathView = path.ToStringView();
1966+
if (rwflags != UV_FS_O_WRONLY) {
1967+
THROW_IF_INSUFFICIENT_PERMISSIONS(
1968+
env, permission::PermissionScope::kFileSystemRead, pathView, false);
1969+
}
1970+
if (rwflags != UV_FS_O_RDONLY || write_as_side_effect) {
1971+
THROW_IF_INSUFFICIENT_PERMISSIONS(
1972+
env, permission::PermissionScope::kFileSystemWrite, pathView, false);
1973+
}
1974+
return true;
1975+
}
1976+
19521977
static void Open(const FunctionCallbackInfo<Value>& args) {
19531978
Environment* env = Environment::GetCurrent(args);
19541979

@@ -1964,22 +1989,7 @@ static void Open(const FunctionCallbackInfo<Value>& args) {
19641989
CHECK(args[2]->IsInt32());
19651990
const int mode = args[2].As<Int32>()->Value();
19661991

1967-
auto pathView = path.ToStringView();
1968-
// Open can be called either in write or read
1969-
if (flags == O_RDWR) {
1970-
// TODO(rafaelgss): it can be optimized to avoid O(2*n)
1971-
THROW_IF_INSUFFICIENT_PERMISSIONS(
1972-
env, permission::PermissionScope::kFileSystemRead, pathView);
1973-
THROW_IF_INSUFFICIENT_PERMISSIONS(
1974-
env, permission::PermissionScope::kFileSystemWrite, pathView);
1975-
} else if ((flags & ~(UV_FS_O_RDONLY | UV_FS_O_SYNC)) == 0) {
1976-
THROW_IF_INSUFFICIENT_PERMISSIONS(
1977-
env, permission::PermissionScope::kFileSystemRead, pathView);
1978-
} else if ((flags & (UV_FS_O_APPEND | UV_FS_O_TRUNC | UV_FS_O_CREAT |
1979-
UV_FS_O_WRONLY)) != 0) {
1980-
THROW_IF_INSUFFICIENT_PERMISSIONS(
1981-
env, permission::PermissionScope::kFileSystemWrite, pathView);
1982-
}
1992+
if (!CheckOpenPermissions(env, path, flags)) return;
19831993

19841994
FSReqBase* req_wrap_async = GetReqWrap(args, 3);
19851995
if (req_wrap_async != nullptr) { // open(path, flags, mode, req)
@@ -2010,31 +2020,14 @@ static void OpenFileHandle(const FunctionCallbackInfo<Value>& args) {
20102020

20112021
BufferValue path(isolate, args[0]);
20122022
CHECK_NOT_NULL(*path);
2013-
auto pathView = path.ToStringView();
2014-
THROW_IF_INSUFFICIENT_PERMISSIONS(
2015-
env, permission::PermissionScope::kFileSystemRead, pathView);
20162023

20172024
CHECK(args[1]->IsInt32());
20182025
const int flags = args[1].As<Int32>()->Value();
20192026

20202027
CHECK(args[2]->IsInt32());
20212028
const int mode = args[2].As<Int32>()->Value();
20222029

2023-
// Open can be called either in write or read
2024-
if (flags == O_RDWR) {
2025-
// TODO(rafaelgss): it can be optimized to avoid O(2*n)
2026-
THROW_IF_INSUFFICIENT_PERMISSIONS(
2027-
env, permission::PermissionScope::kFileSystemRead, pathView);
2028-
THROW_IF_INSUFFICIENT_PERMISSIONS(
2029-
env, permission::PermissionScope::kFileSystemWrite, pathView);
2030-
} else if ((flags & ~(UV_FS_O_RDONLY | UV_FS_O_SYNC)) == 0) {
2031-
THROW_IF_INSUFFICIENT_PERMISSIONS(
2032-
env, permission::PermissionScope::kFileSystemRead, pathView);
2033-
} else if ((flags & (UV_FS_O_APPEND | UV_FS_O_TRUNC | UV_FS_O_CREAT |
2034-
UV_FS_O_WRONLY)) != 0) {
2035-
THROW_IF_INSUFFICIENT_PERMISSIONS(
2036-
env, permission::PermissionScope::kFileSystemWrite, pathView);
2037-
}
2030+
if (!CheckOpenPermissions(env, path, flags)) return;
20382031

20392032
FSReqBase* req_wrap_async = GetReqWrap(args, 3);
20402033
if (req_wrap_async != nullptr) { // openFileHandle(path, flags, mode, req)

test/parallel/test-permission-deny-fs-read.js

+15
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,21 @@ const gid = os.userInfo().gid;
249249
fs.open(regularFile, 'r', (err) => {
250250
assert.ifError(err);
251251
});
252+
253+
// Extra flags should not enable trivially bypassing all restrictions.
254+
// See https://github.com/nodejs/node/issues/47090.
255+
assert.throws(() => {
256+
fs.openSync(blockedFile, fs.constants.O_RDONLY | fs.constants.O_NOCTTY);
257+
}, {
258+
code: 'ERR_ACCESS_DENIED',
259+
permission: 'FileSystemRead',
260+
});
261+
assert.throws(() => {
262+
fs.open(blockedFile, fs.constants.O_RDWR | 0x10000000, common.mustNotCall());
263+
}, {
264+
code: 'ERR_ACCESS_DENIED',
265+
permission: 'FileSystemRead',
266+
});
252267
}
253268

254269
// fs.opendir

test/parallel/test-permission-deny-fs-write.js

+29
Original file line numberDiff line numberDiff line change
@@ -238,3 +238,32 @@ const regularFile = fixtures.path('permission', 'deny', 'regular-file.md');
238238
resource: path.toNamespacedPath(blockedFile),
239239
}));
240240
}
241+
242+
// fs.open
243+
{
244+
// Extra flags should not enable trivially bypassing all restrictions.
245+
// See https://github.com/nodejs/node/issues/47090.
246+
assert.throws(() => {
247+
fs.open(blockedFile, fs.constants.O_RDWR | 0x10000000, common.mustNotCall());
248+
}, {
249+
code: 'ERR_ACCESS_DENIED',
250+
permission: 'FileSystemWrite',
251+
});
252+
assert.rejects(async () => {
253+
await fs.promises.open(blockedFile, fs.constants.O_RDWR | fs.constants.O_NOFOLLOW);
254+
}, {
255+
code: 'ERR_ACCESS_DENIED',
256+
permission: 'FileSystemWrite',
257+
});
258+
if (common.isWindows) {
259+
// In particular, on Windows, the permission system should not blindly let
260+
// code delete write-protected files.
261+
const O_TEMPORARY = 0x40;
262+
assert.throws(() => {
263+
fs.openSync(blockedFile, fs.constants.O_RDONLY | O_TEMPORARY);
264+
}, {
265+
code: 'ERR_ACCESS_DENIED',
266+
permission: 'FileSystemWrite'
267+
});
268+
}
269+
}

0 commit comments

Comments
 (0)