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/);
 }