Skip to content

Commit d6b6051

Browse files
RafaelGSSjuanarbol
authored andcommitted
permission: include permission check on lib/fs/promises
PR-URL: nodejs-private/node-private#795 Reviewed-By: Anna Henningsen <anna@addaleax.net> CVE-ID: CVE-2026-21716
1 parent bfdecef commit d6b6051

File tree

3 files changed

+416
-6
lines changed

3 files changed

+416
-6
lines changed

lib/internal/fs/promises.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const {
1717
Symbol,
1818
SymbolAsyncDispose,
1919
Uint8Array,
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
AbortError,
@@ -1018,8 +1021,13 @@ async function fstat(handle, options = { bigint: false }) {
10181021
}
10191022

10201023
async function lstat(path, options = { bigint: false }) {
1024+
path = getValidatedPath(path);
1025+
if (permission.isEnabled() && !permission.has('fs.read', path)) {
1026+
const resource = pathModule.toNamespacedPath(BufferIsBuffer(path) ? BufferToString(path) : path);
1027+
throw new ERR_ACCESS_DENIED('Access to this API has been restricted', 'FileSystemRead', resource);
1028+
}
10211029
const result = await PromisePrototypeThen(
1022-
binding.lstat(getValidatedPath(path), options.bigint, kUsePromises),
1030+
binding.lstat(path, options.bigint, kUsePromises),
10231031
undefined,
10241032
handleErrorFromBinding,
10251033
);
@@ -1063,6 +1071,9 @@ async function unlink(path) {
10631071
}
10641072

10651073
async function fchmod(handle, mode) {
1074+
if (permission.isEnabled()) {
1075+
throw new ERR_ACCESS_DENIED('fchmod API is disabled when Permission Model is enabled.');
1076+
}
10661077
mode = parseFileMode(mode, 'mode');
10671078
return await PromisePrototypeThen(
10681079
binding.fchmod(handle.fd, mode, kUsePromises),
@@ -1103,6 +1114,9 @@ async function lchown(path, uid, gid) {
11031114
async function fchown(handle, uid, gid) {
11041115
validateInteger(uid, 'uid', -1, kMaxUserId);
11051116
validateInteger(gid, 'gid', -1, kMaxUserId);
1117+
if (permission.isEnabled()) {
1118+
throw new ERR_ACCESS_DENIED('fchown API is disabled when Permission Model is enabled.');
1119+
}
11061120
return await PromisePrototypeThen(
11071121
binding.fchown(handle.fd, uid, gid, kUsePromises),
11081122
undefined,

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
const { pathToFileURL } = require('url');
911

@@ -469,6 +471,204 @@ const regularFile = __filename;
469471
}));
470472
}
471473

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

0 commit comments

Comments
 (0)