Skip to content

Commit b4b887c

Browse files
committed
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 <matteo.collina@gmail.com> Reviewed-By: Anna Henningsen <anna@addaleax.net> CVE-ID: CVE-2025-55132
1 parent fc996fd commit b4b887c

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
@@ -1223,6 +1223,11 @@ function rmSync(path, options) {
12231223
function fdatasync(fd, callback) {
12241224
const req = new FSReqCallback();
12251225
req.oncomplete = makeCallback(callback);
1226+
1227+
if (permission.isEnabled()) {
1228+
callback(new ERR_ACCESS_DENIED('fdatasync API is disabled when Permission Model is enabled.'));
1229+
return;
1230+
}
12261231
binding.fdatasync(fd, req);
12271232
}
12281233

@@ -1234,6 +1239,9 @@ function fdatasync(fd, callback) {
12341239
* @returns {void}
12351240
*/
12361241
function fdatasyncSync(fd) {
1242+
if (permission.isEnabled()) {
1243+
throw new ERR_ACCESS_DENIED('fdatasync API is disabled when Permission Model is enabled.');
1244+
}
12371245
binding.fdatasync(fd);
12381246
}
12391247

@@ -1247,6 +1255,10 @@ function fdatasyncSync(fd) {
12471255
function fsync(fd, callback) {
12481256
const req = new FSReqCallback();
12491257
req.oncomplete = makeCallback(callback);
1258+
if (permission.isEnabled()) {
1259+
callback(new ERR_ACCESS_DENIED('fsync API is disabled when Permission Model is enabled.'));
1260+
return;
1261+
}
12501262
binding.fsync(fd, req);
12511263
}
12521264

@@ -1257,6 +1269,9 @@ function fsync(fd, callback) {
12571269
* @returns {void}
12581270
*/
12591271
function fsyncSync(fd) {
1272+
if (permission.isEnabled()) {
1273+
throw new ERR_ACCESS_DENIED('fsync API is disabled when Permission Model is enabled.');
1274+
}
12601275
binding.fsync(fd);
12611276
}
12621277

@@ -2187,6 +2202,11 @@ function futimes(fd, atime, mtime, callback) {
21872202
mtime = toUnixTimestamp(mtime, 'mtime');
21882203
callback = makeCallback(callback);
21892204

2205+
if (permission.isEnabled()) {
2206+
callback(new ERR_ACCESS_DENIED('futimes API is disabled when Permission Model is enabled.'));
2207+
return;
2208+
}
2209+
21902210
const req = new FSReqCallback();
21912211
req.oncomplete = callback;
21922212
binding.futimes(fd, atime, mtime, req);
@@ -2202,6 +2222,10 @@ function futimes(fd, atime, mtime, callback) {
22022222
* @returns {void}
22032223
*/
22042224
function futimesSync(fd, atime, mtime) {
2225+
if (permission.isEnabled()) {
2226+
throw new ERR_ACCESS_DENIED('futimes API is disabled when Permission Model is enabled.');
2227+
}
2228+
22052229
binding.futimes(
22062230
fd,
22072231
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)