Skip to content

Commit ebbf942

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 c611ea6 commit ebbf942

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
@@ -1261,6 +1261,11 @@ function rmSync(path, options) {
12611261
function fdatasync(fd, callback) {
12621262
const req = new FSReqCallback();
12631263
req.oncomplete = makeCallback(callback);
1264+
1265+
if (permission.isEnabled()) {
1266+
callback(new ERR_ACCESS_DENIED('fdatasync API is disabled when Permission Model is enabled.'));
1267+
return;
1268+
}
12641269
binding.fdatasync(fd, req);
12651270
}
12661271

@@ -1272,6 +1277,9 @@ function fdatasync(fd, callback) {
12721277
* @returns {void}
12731278
*/
12741279
function fdatasyncSync(fd) {
1280+
if (permission.isEnabled()) {
1281+
throw new ERR_ACCESS_DENIED('fdatasync API is disabled when Permission Model is enabled.');
1282+
}
12751283
binding.fdatasync(fd);
12761284
}
12771285

@@ -1285,6 +1293,10 @@ function fdatasyncSync(fd) {
12851293
function fsync(fd, callback) {
12861294
const req = new FSReqCallback();
12871295
req.oncomplete = makeCallback(callback);
1296+
if (permission.isEnabled()) {
1297+
callback(new ERR_ACCESS_DENIED('fsync API is disabled when Permission Model is enabled.'));
1298+
return;
1299+
}
12881300
binding.fsync(fd, req);
12891301
}
12901302

@@ -1295,6 +1307,9 @@ function fsync(fd, callback) {
12951307
* @returns {void}
12961308
*/
12971309
function fsyncSync(fd) {
1310+
if (permission.isEnabled()) {
1311+
throw new ERR_ACCESS_DENIED('fsync API is disabled when Permission Model is enabled.');
1312+
}
12981313
binding.fsync(fd);
12991314
}
13001315

@@ -2221,6 +2236,11 @@ function futimes(fd, atime, mtime, callback) {
22212236
mtime = toUnixTimestamp(mtime, 'mtime');
22222237
callback = makeCallback(callback);
22232238

2239+
if (permission.isEnabled()) {
2240+
callback(new ERR_ACCESS_DENIED('futimes API is disabled when Permission Model is enabled.'));
2241+
return;
2242+
}
2243+
22242244
const req = new FSReqCallback();
22252245
req.oncomplete = callback;
22262246
binding.futimes(fd, atime, mtime, req);
@@ -2236,6 +2256,10 @@ function futimes(fd, atime, mtime, callback) {
22362256
* @returns {void}
22372257
*/
22382258
function futimesSync(fd, atime, mtime) {
2259+
if (permission.isEnabled()) {
2260+
throw new ERR_ACCESS_DENIED('futimes API is disabled when Permission Model is enabled.');
2261+
}
2262+
22392263
binding.futimes(
22402264
fd,
22412265
toUnixTimestamp(atime, 'atime'),

test/fixtures/permission/fs-write.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -573,3 +573,48 @@ const relativeProtectedFolder = process.env.RELATIVEBLOCKEDFOLDER;
573573
code: 'ERR_ACCESS_DENIED',
574574
});
575575
}
576+
577+
// fs.utimes with read-only fd
578+
{
579+
assert.throws(() => {
580+
// blocked file is allowed to read
581+
const fd = fs.openSync(blockedFile, 'r');
582+
const date = new Date();
583+
date.setFullYear(2100,0,1);
584+
585+
fs.futimes(fd, date, date, common.expectsError({
586+
code: 'ERR_ACCESS_DENIED',
587+
}));
588+
fs.futimesSync(fd, date, date);
589+
}, {
590+
code: 'ERR_ACCESS_DENIED',
591+
});
592+
}
593+
594+
// fs.fdatasync with read-only fd
595+
{
596+
assert.throws(() => {
597+
// blocked file is allowed to read
598+
const fd = fs.openSync(blockedFile, 'r');
599+
fs.fdatasync(fd, common.expectsError({
600+
code: 'ERR_ACCESS_DENIED',
601+
}));
602+
fs.fdatasyncSync(fd);
603+
}, {
604+
code: 'ERR_ACCESS_DENIED',
605+
});
606+
}
607+
608+
// fs.fsync with read-only fd
609+
{
610+
assert.throws(() => {
611+
// blocked file is allowed to read
612+
const fd = fs.openSync(blockedFile, 'r');
613+
fs.fsync(fd, common.expectsError({
614+
code: 'ERR_ACCESS_DENIED',
615+
}));
616+
fs.fsyncSync(fd);
617+
}, {
618+
code: 'ERR_ACCESS_DENIED',
619+
});
620+
}

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

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

0 commit comments

Comments
 (0)