Skip to content

Commit 89adaa2

Browse files
RafaelGSSmarco-ippolito
authored andcommitted
lib: disable futimes when permission model is enabled
Refs: https://hackerone.com/reports/3390084 PR-URL: nodejs-private/node-private#748 Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Anna Henningsen <[email protected]> CVE-ID: CVE-2025-55132
1 parent 2ac18ce commit 89adaa2

File tree

3 files changed

+85
-1
lines changed

3 files changed

+85
-1
lines changed

lib/fs.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1247,6 +1247,11 @@ function rmSync(path, options) {
12471247
function fdatasync(fd, callback) {
12481248
const req = new FSReqCallback();
12491249
req.oncomplete = makeCallback(callback);
1250+
1251+
if (permission.isEnabled()) {
1252+
callback(new ERR_ACCESS_DENIED('fdatasync API is disabled when Permission Model is enabled.'));
1253+
return;
1254+
}
12501255
binding.fdatasync(fd, req);
12511256
}
12521257

@@ -1258,6 +1263,9 @@ function fdatasync(fd, callback) {
12581263
* @returns {void}
12591264
*/
12601265
function fdatasyncSync(fd) {
1266+
if (permission.isEnabled()) {
1267+
throw new ERR_ACCESS_DENIED('fdatasync API is disabled when Permission Model is enabled.');
1268+
}
12611269
binding.fdatasync(fd);
12621270
}
12631271

@@ -1271,6 +1279,10 @@ function fdatasyncSync(fd) {
12711279
function fsync(fd, callback) {
12721280
const req = new FSReqCallback();
12731281
req.oncomplete = makeCallback(callback);
1282+
if (permission.isEnabled()) {
1283+
callback(new ERR_ACCESS_DENIED('fsync API is disabled when Permission Model is enabled.'));
1284+
return;
1285+
}
12741286
binding.fsync(fd, req);
12751287
}
12761288

@@ -1281,6 +1293,9 @@ function fsync(fd, callback) {
12811293
* @returns {void}
12821294
*/
12831295
function fsyncSync(fd) {
1296+
if (permission.isEnabled()) {
1297+
throw new ERR_ACCESS_DENIED('fsync API is disabled when Permission Model is enabled.');
1298+
}
12841299
binding.fsync(fd);
12851300
}
12861301

@@ -2211,6 +2226,11 @@ function futimes(fd, atime, mtime, callback) {
22112226
mtime = toUnixTimestamp(mtime, 'mtime');
22122227
callback = makeCallback(callback);
22132228

2229+
if (permission.isEnabled()) {
2230+
callback(new ERR_ACCESS_DENIED('futimes API is disabled when Permission Model is enabled.'));
2231+
return;
2232+
}
2233+
22142234
const req = new FSReqCallback();
22152235
req.oncomplete = callback;
22162236
binding.futimes(fd, atime, mtime, req);
@@ -2226,6 +2246,10 @@ function futimes(fd, atime, mtime, callback) {
22262246
* @returns {void}
22272247
*/
22282248
function futimesSync(fd, atime, mtime) {
2249+
if (permission.isEnabled()) {
2250+
throw new ERR_ACCESS_DENIED('futimes API is disabled when Permission Model is enabled.');
2251+
}
2252+
22292253
binding.futimes(
22302254
fd,
22312255
toUnixTimestamp(atime, 'atime'),

test/fixtures/permission/fs-write.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -584,3 +584,48 @@ const relativeProtectedFolder = process.env.RELATIVEBLOCKEDFOLDER;
584584
code: 'ERR_ACCESS_DENIED',
585585
});
586586
}
587+
588+
// fs.utimes with read-only fd
589+
{
590+
assert.throws(() => {
591+
// blocked file is allowed to read
592+
const fd = fs.openSync(blockedFile, 'r');
593+
const date = new Date();
594+
date.setFullYear(2100,0,1);
595+
596+
fs.futimes(fd, date, date, common.expectsError({
597+
code: 'ERR_ACCESS_DENIED',
598+
}));
599+
fs.futimesSync(fd, date, date);
600+
}, {
601+
code: 'ERR_ACCESS_DENIED',
602+
});
603+
}
604+
605+
// fs.fdatasync with read-only fd
606+
{
607+
assert.throws(() => {
608+
// blocked file is allowed to read
609+
const fd = fs.openSync(blockedFile, 'r');
610+
fs.fdatasync(fd, common.expectsError({
611+
code: 'ERR_ACCESS_DENIED',
612+
}));
613+
fs.fdatasyncSync(fd);
614+
}, {
615+
code: 'ERR_ACCESS_DENIED',
616+
});
617+
}
618+
619+
// fs.fsync with read-only fd
620+
{
621+
assert.throws(() => {
622+
// blocked file is allowed to read
623+
const fd = fs.openSync(blockedFile, 'r');
624+
fs.fsync(fd, common.expectsError({
625+
code: 'ERR_ACCESS_DENIED',
626+
}));
627+
fs.fsyncSync(fd);
628+
}, {
629+
code: 'ERR_ACCESS_DENIED',
630+
});
631+
}

test/parallel/test-permission-fs-supported.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,22 @@ const ignoreList = [
8282
'unwatchFile',
8383
...syncAndAsyncAPI('lstat'),
8484
...syncAndAsyncAPI('realpath'),
85-
// fd required methods
85+
// File descriptor–based metadata operations
86+
//
87+
// The kernel does not allow opening a file descriptor for an inode
88+
// with write access if the inode itself is read-only. However, it still
89+
// permits modifying the inode’s metadata (e.g., permission bits, ownership,
90+
// timestamps) because you own the file. These changes can be made either
91+
// by referring to the file by name (e.g., chmod) or through any existing
92+
// file descriptor that identifies the same inode (e.g., fchmod).
93+
//
94+
// If the kernel required write access to change metadata, it would be
95+
// impossible to modify the permissions of a file once it was made read-only.
96+
// For that reason, syscalls such as fchmod, fchown, and futimes bypass
97+
// the file descriptor’s access mode. Even a read-only ('r') descriptor
98+
// can still update metadata. To prevent unintended modifications,
99+
// these APIs are therefore blocked by default when permission model is
100+
// enabled.
86101
...syncAndAsyncAPI('close'),
87102
...syncAndAsyncAPI('fchown'),
88103
...syncAndAsyncAPI('fchmod'),

0 commit comments

Comments
 (0)