Skip to content

Commit fb43893

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 <matteo.collina@gmail.com> Reviewed-By: Anna Henningsen <anna@addaleax.net> CVE-ID: CVE-2025-55132
1 parent 6ea77ad commit fb43893

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

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

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

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

@@ -2188,6 +2203,11 @@ function futimes(fd, atime, mtime, callback) {
21882203
mtime = toUnixTimestamp(mtime, 'mtime');
21892204
callback = makeCallback(callback);
21902205

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