Commit 6b88896652d for nodejs
commit 6b88896652dabf01780e0cec785914f80fcb8600
Author: RafaelGSS <rafael.nunu@hotmail.com>
Date: Fri Aug 28 10:37:46 2026 -0300
permission: check permission model in realpathSync
The equivalent entry points (fs.realpathSync.native(),
fs.promises.realpath(), fs.lstatSync(), fs.existsSync())
already deny access for the same paths
Signed-off-by: RafaelGSS <rafael.nunu@hotmail.com>
PR-URL: https://github.com/nodejs-private/node-private/pull/973
Refs: https://hackerone.com/reports/3838675
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
CVE-ID: CVE-2026-106073
diff --git a/lib/fs.js b/lib/fs.js
index c0f7e6f0adf..9f6cfbddb65 100644
--- a/lib/fs.js
+++ b/lib/fs.js
@@ -3368,6 +3368,17 @@ function realpathSync(p, options) {
validatePath(p);
p = pathModule.resolve(p);
+ // Check the resolved input path up front so that the lstat walk below cannot
+ // be used to probe the existence of paths outside the granted set.
+ if (permission.isEnabled() && !permission.has('fs.read', p) &&
+ !permission.isAuditMode()) {
+ throw new ERR_ACCESS_DENIED(
+ 'Access to this API has been restricted. Use --allow-fs-read to manage permissions.',
+ 'FileSystemRead',
+ p,
+ );
+ }
+
const cache = options[realpathCacheKey];
const maybeCachedResult = cache?.get(p);
if (maybeCachedResult) {
diff --git a/test/common/tmpdir.js b/test/common/tmpdir.js
index d56a7331105..4692e162723 100644
--- a/test/common/tmpdir.js
+++ b/test/common/tmpdir.js
@@ -32,8 +32,20 @@ function rmSync(pathname, useSpawn) {
}
}
+// Under the permission model, fs.realpathSync() requires read access to the
+// path, which tests running with a restricted --allow-fs-read may not grant
+// for NODE_TEST_DIR.
+function resolveTestRoot(dir) {
+ try {
+ return fs.realpathSync(dir);
+ } catch (err) {
+ if (err.code !== 'ERR_ACCESS_DENIED') throw err;
+ return path.resolve(dir);
+ }
+}
+
const testRoot = process.env.NODE_TEST_DIR ?
- fs.realpathSync(process.env.NODE_TEST_DIR) : path.resolve(__dirname, '..');
+ resolveTestRoot(process.env.NODE_TEST_DIR) : path.resolve(__dirname, '..');
// Using a `.` prefixed name, which is the convention for "hidden" on POSIX,
// gets tools to ignore it by default or by simple rules, especially eslint.
diff --git a/test/fixtures/permission/fs-read.js b/test/fixtures/permission/fs-read.js
index 59900f8f03f..2feb64d021b 100644
--- a/test/fixtures/permission/fs-read.js
+++ b/test/fixtures/permission/fs-read.js
@@ -721,6 +721,57 @@ const regularFile = __filename;
});
}
+// fs.realpath (JS implementation, walks the path with lstat)
+{
+ assert.throws(() => {
+ fs.realpathSync(blockedFile);
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemRead',
+ }));
+ assert.throws(() => {
+ fs.realpathSync(bufferBlockedFile);
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemRead',
+ }));
+ assert.throws(() => {
+ fs.realpathSync(path.join(blockedFolder, 'anyfile'));
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemRead',
+ }));
+ // Must not act as a file-existence oracle: a non-existent path inside a
+ // blocked folder must be denied, not reported as ENOENT.
+ assert.throws(() => {
+ fs.realpathSync(path.join(blockedFolder, 'does-not-exist'));
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemRead',
+ }));
+ fs.realpath(blockedFile, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemRead',
+ }));
+ assert.throws(() => {
+ fs.realpathSync(blockedFile);
+ }, common.expectsError({
+ message: 'Access to this API has been restricted. Use --allow-fs-read to manage permissions.',
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemRead',
+ resource: blockedFile,
+ }));
+ assert.rejects(async () => {
+ await fsPromises.realpath(blockedFile);
+ }, common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemRead',
+ })).then(common.mustCall());
+
+ // doesNotThrow
+ assert.strictEqual(typeof fs.realpathSync(regularFile), 'string');
+}
+
// fs.realpath.native
{
fs.realpath.native(blockedFile, common.expectsError({
diff --git a/test/parallel/test-permission-audit-fs-realpath.js b/test/parallel/test-permission-audit-fs-realpath.js
new file mode 100644
index 00000000000..2a158a1a114
--- /dev/null
+++ b/test/parallel/test-permission-audit-fs-realpath.js
@@ -0,0 +1,19 @@
+// Flags: --permission-audit
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const dc = require('diagnostics_channel');
+const fs = require('fs');
+const fixtures = require('../common/fixtures');
+
+const file = fixtures.path('permission', 'deny', 'protected-file.md');
+
+const denied = [];
+dc.subscribe('node:permission-model:fs', common.mustCallAtLeast((message) => {
+ assert.strictEqual(message.permission, 'FileSystemRead');
+ denied.push(message.resource);
+}));
+
+assert.strictEqual(fs.realpathSync(file), file);
+assert.ok(denied.includes(file), `realpathSync reported ${file}`);
diff --git a/test/parallel/test-permission-fs-require.js b/test/parallel/test-permission-fs-require.js
index 8406f9ec052..23c1b7508b0 100644
--- a/test/parallel/test-permission-fs-require.js
+++ b/test/parallel/test-permission-fs-require.js
@@ -44,7 +44,7 @@ const { spawnSync } = require('node:child_process');
);
assert.strictEqual(status, 1, stderr.toString());
- assert.match(stderr.toString(), /Error: Access to this API has been restricted/);
+ assert.match(stderr.toString(), /Error(?: \[ERR_ACCESS_DENIED\])?: Access to this API has been restricted/);
}
{
@@ -78,5 +78,5 @@ const { spawnSync } = require('node:child_process');
);
assert.strictEqual(status, 1, stderr.toString());
- assert.match(stderr.toString(), /Error: Access to this API has been restricted/);
+ assert.match(stderr.toString(), /Error(?: \[ERR_ACCESS_DENIED\])?: Access to this API has been restricted/);
}