Skip to content

Commit 205f1e6

Browse files
committed
permission: handle fs path traversal
PR-URL: nodejs-private/node-private#403 Refs: https://hackerone.com/bugs?subject=nodejs&report_id=1952978 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> CVE-ID: CVE-2023-30584
1 parent e15cc45 commit 205f1e6

File tree

5 files changed

+103
-6
lines changed

5 files changed

+103
-6
lines changed

lib/internal/fs/utils.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -709,7 +709,7 @@ const validatePath = hideStackFrames((path, propName = 'path') => {
709709
// The permission model needs the absolute path for the fs_permission
710710
function possiblyTransformPath(path) {
711711
if (permission.isEnabled()) {
712-
if (typeof path === 'string' && !pathModule.isAbsolute(path)) {
712+
if (typeof path === 'string') {
713713
return pathModule.resolve(path);
714714
}
715715
}

lib/internal/process/permission.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const {
77

88
const permission = internalBinding('permission');
99
const { validateString } = require('internal/validators');
10-
const { isAbsolute, resolve } = require('path');
10+
const { resolve } = require('path');
1111

1212
let experimentalPermission;
1313

@@ -26,9 +26,7 @@ module.exports = ObjectFreeze({
2626
// TODO: add support for WHATWG URLs and Uint8Arrays.
2727
validateString(reference, 'reference');
2828
if (StringPrototypeStartsWith(scope, 'fs')) {
29-
if (!isAbsolute(reference)) {
30-
reference = resolve(reference);
31-
}
29+
reference = resolve(reference);
3230
}
3331
}
3432

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
'use strict'
2+
3+
const common = require('../../common');
4+
5+
const assert = require('assert');
6+
const fs = require('fs');
7+
const path = require('path');
8+
9+
const blockedFolder = process.env.BLOCKEDFOLDER;
10+
const allowedFolder = process.env.ALLOWEDFOLDER;
11+
const traversalPath = allowedFolder + '../file.md'
12+
13+
{
14+
assert.ok(process.permission.has('fs.read', allowedFolder));
15+
assert.ok(process.permission.has('fs.write', allowedFolder));
16+
assert.ok(!process.permission.has('fs.read', blockedFolder));
17+
assert.ok(!process.permission.has('fs.write', blockedFolder));
18+
}
19+
20+
{
21+
assert.throws(() => {
22+
fs.writeFile(traversalPath, 'test', (error) => {
23+
assert.ifError(error);
24+
});
25+
}, common.expectsError({
26+
code: 'ERR_ACCESS_DENIED',
27+
permission: 'FileSystemWrite',
28+
resource: path.toNamespacedPath(path.resolve(traversalPath)),
29+
}));
30+
}
31+
32+
{
33+
assert.throws(() => {
34+
fs.readFile(traversalPath, (error) => {
35+
assert.ifError(error);
36+
});
37+
}, common.expectsError({
38+
code: 'ERR_ACCESS_DENIED',
39+
permission: 'FileSystemRead',
40+
resource: path.toNamespacedPath(path.resolve(traversalPath)),
41+
}));
42+
}
43+
44+
{
45+
assert.ok(!process.permission.has('fs.read', traversalPath));
46+
assert.ok(!process.permission.has('fs.write', traversalPath));
47+
}

test/parallel/test-cli-permission-deny-fs.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,12 @@ const path = require('path');
2727
}
2828

2929
{
30+
const tmpPath = path.resolve('/tmp/');
3031
const { status, stdout } = spawnSync(
3132
process.execPath,
3233
[
3334
'--experimental-permission',
34-
'--allow-fs-write', '/tmp/', '-e',
35+
'--allow-fs-write', tmpPath, '-e',
3536
`console.log(process.permission.has("fs"));
3637
console.log(process.permission.has("fs.read"));
3738
console.log(process.permission.has("fs.write"));
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Flags: --experimental-permission --allow-fs-read=* --allow-fs-write=* --allow-child-process
2+
'use strict';
3+
4+
const common = require('../common');
5+
common.skipIfWorker();
6+
7+
const fixtures = require('../common/fixtures');
8+
if (!common.canCreateSymLink())
9+
common.skip('insufficient privileges');
10+
if (!common.hasCrypto)
11+
common.skip('no crypto');
12+
13+
const assert = require('assert');
14+
const fs = require('fs');
15+
const { spawnSync } = require('child_process');
16+
const path = require('path');
17+
const tmpdir = require('../common/tmpdir');
18+
19+
const file = fixtures.path('permission', 'fs-traversal.js');
20+
const blockedFolder = tmpdir.path;
21+
const allowedFolder = path.join(tmpdir.path, 'subdirectory/');
22+
const commonPathWildcard = path.join(__filename, '../../common*');
23+
24+
{
25+
tmpdir.refresh();
26+
fs.mkdirSync(allowedFolder);
27+
}
28+
29+
{
30+
const { status, stderr } = spawnSync(
31+
process.execPath,
32+
[
33+
'--experimental-permission',
34+
`--allow-fs-read=${file},${commonPathWildcard},${allowedFolder}`,
35+
`--allow-fs-write=${allowedFolder}`,
36+
file,
37+
],
38+
{
39+
env: {
40+
...process.env,
41+
BLOCKEDFOLDER: blockedFolder,
42+
ALLOWEDFOLDER: allowedFolder,
43+
},
44+
}
45+
);
46+
assert.strictEqual(status, 0, stderr.toString());
47+
}
48+
49+
{
50+
tmpdir.refresh();
51+
}

0 commit comments

Comments
 (0)