Skip to content

Commit 0123309

Browse files
RafaelGSSaduh95
authored andcommitted
permission: include permission check on lib/fs/promises
PR-URL: nodejs-private/node-private#840 CVE-ID: CVE-2026-21716
1 parent cc3f294 commit 0123309

File tree

5 files changed

+438
-21
lines changed

5 files changed

+438
-21
lines changed

lib/internal/fs/promises.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const {
1717
Symbol,
1818
Uint8Array,
1919
FunctionPrototypeBind,
20+
uncurryThis,
2021
} = primordials;
2122

2223
const { fs: constants } = internalBinding('constants');
@@ -30,6 +31,8 @@ const {
3031

3132
const binding = internalBinding('fs');
3233
const { Buffer } = require('buffer');
34+
const { isBuffer: BufferIsBuffer } = Buffer;
35+
const BufferToString = uncurryThis(Buffer.prototype.toString);
3336

3437
const {
3538
codes: {
@@ -1012,6 +1015,10 @@ async function fstat(handle, options = { bigint: false }) {
10121015

10131016
async function lstat(path, options = { bigint: false }) {
10141017
path = getValidatedPath(path);
1018+
if (permission.isEnabled() && !permission.has('fs.read', path)) {
1019+
const resource = pathModule.toNamespacedPath(BufferIsBuffer(path) ? BufferToString(path) : path);
1020+
throw new ERR_ACCESS_DENIED('Access to this API has been restricted', 'FileSystemRead', resource);
1021+
}
10151022
const result = await PromisePrototypeThen(
10161023
binding.lstat(pathModule.toNamespacedPath(path),
10171024
options.bigint, kUsePromises),
@@ -1065,6 +1072,9 @@ async function unlink(path) {
10651072
}
10661073

10671074
async function fchmod(handle, mode) {
1075+
if (permission.isEnabled()) {
1076+
throw new ERR_ACCESS_DENIED('fchmod API is disabled when Permission Model is enabled.');
1077+
}
10681078
mode = parseFileMode(mode, 'mode');
10691079
return await PromisePrototypeThen(
10701080
binding.fchmod(handle.fd, mode, kUsePromises),
@@ -1105,6 +1115,9 @@ async function lchown(path, uid, gid) {
11051115
async function fchown(handle, uid, gid) {
11061116
validateInteger(uid, 'uid', -1, kMaxUserId);
11071117
validateInteger(gid, 'gid', -1, kMaxUserId);
1118+
if (permission.isEnabled()) {
1119+
throw new ERR_ACCESS_DENIED('fchown API is disabled when Permission Model is enabled.');
1120+
}
11081121
return await PromisePrototypeThen(
11091122
binding.fchown(handle.fd, uid, gid, kUsePromises),
11101123
undefined,

src/node_file-inl.h

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -287,21 +287,27 @@ FSReqBase* GetReqWrap(const v8::FunctionCallbackInfo<v8::Value>& args,
287287
int index,
288288
bool use_bigint) {
289289
v8::Local<v8::Value> value = args[index];
290+
FSReqBase* result = nullptr;
290291
if (value->IsObject()) {
291-
return Unwrap<FSReqBase>(value.As<v8::Object>());
292-
}
293-
294-
Realm* realm = Realm::GetCurrent(args);
295-
BindingData* binding_data = realm->GetBindingData<BindingData>();
296-
297-
if (value->StrictEquals(realm->isolate_data()->fs_use_promises_symbol())) {
298-
if (use_bigint) {
299-
return FSReqPromise<AliasedBigInt64Array>::New(binding_data, use_bigint);
300-
} else {
301-
return FSReqPromise<AliasedFloat64Array>::New(binding_data, use_bigint);
292+
result = Unwrap<FSReqBase>(value.As<v8::Object>());
293+
} else {
294+
Realm* realm = Realm::GetCurrent(args);
295+
BindingData* binding_data = realm->GetBindingData<BindingData>();
296+
297+
if (value->StrictEquals(realm->isolate_data()->fs_use_promises_symbol())) {
298+
if (use_bigint) {
299+
result =
300+
FSReqPromise<AliasedBigInt64Array>::New(binding_data, use_bigint);
301+
} else {
302+
result =
303+
FSReqPromise<AliasedFloat64Array>::New(binding_data, use_bigint);
304+
}
302305
}
303306
}
304-
return nullptr;
307+
if (result != nullptr) {
308+
result->SetReturnValue(args);
309+
}
310+
return result;
305311
}
306312

307313
// Returns nullptr if the operation fails from the start.
@@ -320,10 +326,7 @@ FSReqBase* AsyncDestCall(Environment* env, FSReqBase* req_wrap,
320326
uv_req->path = nullptr;
321327
after(uv_req); // after may delete req_wrap if there is an error
322328
req_wrap = nullptr;
323-
} else {
324-
req_wrap->SetReturnValue(args);
325329
}
326-
327330
return req_wrap;
328331
}
329332

src/node_file.cc

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2424,8 +2424,6 @@ static void WriteString(const FunctionCallbackInfo<Value>& args) {
24242424
uv_req->path = nullptr;
24252425
AfterInteger(uv_req); // after may delete req_wrap_async if there is
24262426
// an error
2427-
} else {
2428-
req_wrap_async->SetReturnValue(args);
24292427
}
24302428
} else { // write(fd, string, pos, enc, undefined, ctx)
24312429
CHECK_EQ(argc, 6);

test/fixtures/permission/fs-read.js

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ const common = require('../../common');
44

55
const assert = require('assert');
66
const fs = require('fs');
7+
const fsPromises = require('node:fs/promises');
8+
79
const path = require('path');
810

911
const blockedFile = process.env.BLOCKEDFILE;
@@ -453,6 +455,204 @@ const regularFile = __filename;
453455
}));
454456
}
455457

458+
// fsPromises.readFile
459+
{
460+
assert.rejects(async () => {
461+
await fsPromises.readFile(blockedFile);
462+
}, common.expectsError({
463+
code: 'ERR_ACCESS_DENIED',
464+
permission: 'FileSystemRead',
465+
resource: path.toNamespacedPath(blockedFile),
466+
})).then(common.mustCall());
467+
assert.rejects(async () => {
468+
await fsPromises.readFile(blockedFileURL);
469+
}, common.expectsError({
470+
code: 'ERR_ACCESS_DENIED',
471+
permission: 'FileSystemRead',
472+
resource: path.toNamespacedPath(blockedFile),
473+
})).then(common.mustCall());
474+
}
475+
476+
// fsPromises.stat
477+
{
478+
assert.rejects(async () => {
479+
await fsPromises.stat(blockedFile);
480+
}, common.expectsError({
481+
code: 'ERR_ACCESS_DENIED',
482+
permission: 'FileSystemRead',
483+
resource: path.toNamespacedPath(blockedFile),
484+
})).then(common.mustCall());
485+
assert.rejects(async () => {
486+
await fsPromises.stat(blockedFileURL);
487+
}, common.expectsError({
488+
code: 'ERR_ACCESS_DENIED',
489+
permission: 'FileSystemRead',
490+
resource: path.toNamespacedPath(blockedFile),
491+
})).then(common.mustCall());
492+
assert.rejects(async () => {
493+
await fsPromises.stat(path.join(blockedFolder, 'anyfile'));
494+
}, common.expectsError({
495+
code: 'ERR_ACCESS_DENIED',
496+
permission: 'FileSystemRead',
497+
resource: path.toNamespacedPath(path.join(blockedFolder, 'anyfile')),
498+
})).then(common.mustCall());
499+
}
500+
501+
// fsPromises.access
502+
{
503+
assert.rejects(async () => {
504+
await fsPromises.access(blockedFile, fs.constants.R_OK);
505+
}, common.expectsError({
506+
code: 'ERR_ACCESS_DENIED',
507+
permission: 'FileSystemRead',
508+
resource: path.toNamespacedPath(blockedFile),
509+
})).then(common.mustCall());
510+
assert.rejects(async () => {
511+
await fsPromises.access(blockedFileURL, fs.constants.R_OK);
512+
}, common.expectsError({
513+
code: 'ERR_ACCESS_DENIED',
514+
permission: 'FileSystemRead',
515+
resource: path.toNamespacedPath(blockedFile),
516+
})).then(common.mustCall());
517+
assert.rejects(async () => {
518+
await fsPromises.access(path.join(blockedFolder, 'anyfile'), fs.constants.R_OK);
519+
}, common.expectsError({
520+
code: 'ERR_ACCESS_DENIED',
521+
permission: 'FileSystemRead',
522+
resource: path.toNamespacedPath(path.join(blockedFolder, 'anyfile')),
523+
})).then(common.mustCall());
524+
}
525+
526+
// fsPromises.copyFile
527+
{
528+
assert.rejects(async () => {
529+
await fsPromises.copyFile(blockedFile, path.join(blockedFolder, 'any-other-file'));
530+
}, common.expectsError({
531+
code: 'ERR_ACCESS_DENIED',
532+
permission: 'FileSystemRead',
533+
resource: path.toNamespacedPath(blockedFile),
534+
})).then(common.mustCall());
535+
assert.rejects(async () => {
536+
await fsPromises.copyFile(blockedFileURL, path.join(blockedFolder, 'any-other-file'));
537+
}, common.expectsError({
538+
code: 'ERR_ACCESS_DENIED',
539+
permission: 'FileSystemRead',
540+
resource: path.toNamespacedPath(blockedFile),
541+
})).then(common.mustCall());
542+
}
543+
544+
// fsPromises.cp
545+
{
546+
assert.rejects(async () => {
547+
await fsPromises.cp(blockedFile, path.join(blockedFolder, 'any-other-file'));
548+
}, common.expectsError({
549+
code: 'ERR_ACCESS_DENIED',
550+
permission: 'FileSystemRead',
551+
resource: path.toNamespacedPath(blockedFile),
552+
})).then(common.mustCall());
553+
assert.rejects(async () => {
554+
await fsPromises.cp(blockedFileURL, path.join(blockedFolder, 'any-other-file'));
555+
}, common.expectsError({
556+
code: 'ERR_ACCESS_DENIED',
557+
permission: 'FileSystemRead',
558+
resource: path.toNamespacedPath(blockedFile),
559+
})).then(common.mustCall());
560+
}
561+
562+
// fsPromises.open
563+
{
564+
assert.rejects(async () => {
565+
await fsPromises.open(blockedFile, 'r');
566+
}, common.expectsError({
567+
code: 'ERR_ACCESS_DENIED',
568+
permission: 'FileSystemRead',
569+
resource: path.toNamespacedPath(blockedFile),
570+
})).then(common.mustCall());
571+
assert.rejects(async () => {
572+
await fsPromises.open(blockedFileURL, 'r');
573+
}, common.expectsError({
574+
code: 'ERR_ACCESS_DENIED',
575+
permission: 'FileSystemRead',
576+
resource: path.toNamespacedPath(blockedFile),
577+
})).then(common.mustCall());
578+
assert.rejects(async () => {
579+
await fsPromises.open(path.join(blockedFolder, 'anyfile'), 'r');
580+
}, common.expectsError({
581+
code: 'ERR_ACCESS_DENIED',
582+
permission: 'FileSystemRead',
583+
resource: path.toNamespacedPath(path.join(blockedFolder, 'anyfile')),
584+
})).then(common.mustCall());
585+
}
586+
587+
// fsPromises.opendir
588+
{
589+
assert.rejects(async () => {
590+
await fsPromises.opendir(blockedFolder);
591+
}, common.expectsError({
592+
code: 'ERR_ACCESS_DENIED',
593+
permission: 'FileSystemRead',
594+
resource: path.toNamespacedPath(blockedFolder),
595+
})).then(common.mustCall());
596+
}
597+
598+
// fsPromises.readdir
599+
{
600+
assert.rejects(async () => {
601+
await fsPromises.readdir(blockedFolder);
602+
}, common.expectsError({
603+
code: 'ERR_ACCESS_DENIED',
604+
permission: 'FileSystemRead',
605+
resource: path.toNamespacedPath(blockedFolder),
606+
})).then(common.mustCall());
607+
assert.rejects(async () => {
608+
await fsPromises.readdir(blockedFolder, { recursive: true });
609+
}, common.expectsError({
610+
code: 'ERR_ACCESS_DENIED',
611+
permission: 'FileSystemRead',
612+
resource: path.toNamespacedPath(blockedFolder),
613+
})).then(common.mustCall());
614+
}
615+
616+
// fsPromises.rename
617+
{
618+
assert.rejects(async () => {
619+
await fsPromises.rename(blockedFile, 'newfile');
620+
}, common.expectsError({
621+
code: 'ERR_ACCESS_DENIED',
622+
permission: 'FileSystemRead',
623+
resource: path.toNamespacedPath(blockedFile),
624+
})).then(common.mustCall());
625+
assert.rejects(async () => {
626+
await fsPromises.rename(blockedFileURL, 'newfile');
627+
}, common.expectsError({
628+
code: 'ERR_ACCESS_DENIED',
629+
permission: 'FileSystemRead',
630+
resource: path.toNamespacedPath(blockedFile),
631+
})).then(common.mustCall());
632+
}
633+
634+
// fsPromises.lstat
635+
{
636+
assert.rejects(async () => {
637+
await fsPromises.lstat(blockedFile);
638+
}, common.expectsError({
639+
code: 'ERR_ACCESS_DENIED',
640+
permission: 'FileSystemRead',
641+
})).then(common.mustCall());
642+
assert.rejects(async () => {
643+
await fsPromises.lstat(blockedFileURL);
644+
}, common.expectsError({
645+
code: 'ERR_ACCESS_DENIED',
646+
permission: 'FileSystemRead',
647+
})).then(common.mustCall());
648+
assert.rejects(async () => {
649+
await fsPromises.lstat(path.join(blockedFolder, 'anyfile'));
650+
}, common.expectsError({
651+
code: 'ERR_ACCESS_DENIED',
652+
permission: 'FileSystemRead',
653+
})).then(common.mustCall());
654+
}
655+
456656
// fs.lstat
457657
{
458658
assert.throws(() => {

0 commit comments

Comments
 (0)