Skip to content

Commit 2d550a2

Browse files
tniessenLei Shi
authored and
Lei Shi
committed
permission: do not create symlinks if target is relative
The permission model's security guarantees fall apart in the presence of relative symbolic links. When an application attempts to create a relative symlink, the permission model currently resolves the relative path into an absolute path based on the process's current working directory, checks whether the process has the relevant permissions, and then creates the symlink using the absolute target path. This behavior is plainly incorrect for two reasons: 1. The target path should never be resolved relative to the current working directory. If anything, it should be resolved relative to the symlink's location. (Of course, there is one insane exception to this rule: on Windows, each process has a current working directory per drive, and symlinks can be created with a target path relative to the current working directory of a specific drive. In that case, the relative path will be resolved relative to the current working directory for the respective drive, and the symlink will be created on disk with the resulting absolute path. Other relative symlinks will be stored as-is.) 2. Silently creating an absolute symlink when the user requested a relative symlink is wrong. The user may (or may not) rely on the symlink being relative. For example, npm heavily relies on relative symbolic links such that node_modules directories can be moved around without breaking. Because we don't know the user's intentions, we don't know if creating an absolute symlink instead of a relative symlink is acceptable. This patch prevents the faulty behavior by not (incorrectly) resolving relative symlink targets when the permission model is enabled, and by instead simply refusing the create any relative symlinks. The fs APIs accept Uint8Array objects for paths to be able to handle arbitrary file name charsets, however, checking whether such an object represents a relative part in a reliable and portable manner is tricky. Other parts of the permission model incorrectly convert such objects to strings and then back to an Uint8Array (see 1f64147), however, for now, this bug fix will simply throw on non-string symlink targets when the permission model is enabled. (The permission model already breaks existing applications in various ways, so this shouldn't be too dramatic.) PR-URL: nodejs#49156 Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com> Reviewed-By: Michael Dawson <midawson@redhat.com> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent 38f3754 commit 2d550a2

File tree

3 files changed

+63
-0
lines changed

3 files changed

+63
-0
lines changed

lib/fs.js

+22
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ const {
5858
} = constants;
5959

6060
const pathModule = require('path');
61+
const { isAbsolute } = pathModule;
6162
const { isArrayBufferView } = require('internal/util/types');
6263

6364
const binding = internalBinding('fs');
@@ -68,6 +69,7 @@ const { Buffer } = require('buffer');
6869
const {
6970
aggregateTwoErrors,
7071
codes: {
72+
ERR_ACCESS_DENIED,
7173
ERR_FS_FILE_TOO_LARGE,
7274
ERR_INVALID_ARG_VALUE,
7375
},
@@ -143,6 +145,8 @@ const {
143145
kValidateObjectAllowNullable,
144146
} = require('internal/validators');
145147

148+
const permission = require('internal/process/permission');
149+
146150
let truncateWarn = true;
147151
let fs;
148152

@@ -1742,6 +1746,15 @@ function symlink(target, path, type_, callback_) {
17421746
const type = (typeof type_ === 'string' ? type_ : null);
17431747
const callback = makeCallback(arguments[arguments.length - 1]);
17441748

1749+
if (permission.isEnabled()) {
1750+
// The permission model's security guarantees fall apart in the presence of
1751+
// relative symbolic links. Thus, we have to prevent their creation.
1752+
if (typeof target !== 'string' || !isAbsolute(toPathIfFileURL(target))) {
1753+
callback(new ERR_ACCESS_DENIED('relative symbolic link target'));
1754+
return;
1755+
}
1756+
}
1757+
17451758
target = getValidatedPath(target, 'target');
17461759
path = getValidatedPath(path);
17471760

@@ -1798,6 +1811,15 @@ function symlinkSync(target, path, type) {
17981811
type = 'dir';
17991812
}
18001813
}
1814+
1815+
if (permission.isEnabled()) {
1816+
// The permission model's security guarantees fall apart in the presence of
1817+
// relative symbolic links. Thus, we have to prevent their creation.
1818+
if (typeof target !== 'string' || !isAbsolute(toPathIfFileURL(target))) {
1819+
throw new ERR_ACCESS_DENIED('relative symbolic link target');
1820+
}
1821+
}
1822+
18011823
target = getValidatedPath(target, 'target');
18021824
path = getValidatedPath(path);
18031825

lib/internal/fs/promises.js

+14
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const { Buffer } = require('buffer');
3333

3434
const {
3535
codes: {
36+
ERR_ACCESS_DENIED,
3637
ERR_FS_FILE_TOO_LARGE,
3738
ERR_INVALID_ARG_VALUE,
3839
ERR_INVALID_STATE,
@@ -86,6 +87,8 @@ const {
8687
kValidateObjectAllowNullable,
8788
} = require('internal/validators');
8889
const pathModule = require('path');
90+
const { isAbsolute } = pathModule;
91+
const { toPathIfFileURL } = require('internal/url');
8992
const {
9093
kEmptyObject,
9194
lazyDOMException,
@@ -98,6 +101,8 @@ const nonNativeWatcher = require('internal/fs/recursive_watch');
98101
const { isIterable } = require('internal/streams/utils');
99102
const assert = require('internal/assert');
100103

104+
const permission = require('internal/process/permission');
105+
101106
const kHandle = Symbol('kHandle');
102107
const kFd = Symbol('kFd');
103108
const kRefs = Symbol('kRefs');
@@ -974,6 +979,15 @@ async function symlink(target, path, type_) {
974979
type = 'file';
975980
}
976981
}
982+
983+
if (permission.isEnabled()) {
984+
// The permission model's security guarantees fall apart in the presence of
985+
// relative symbolic links. Thus, we have to prevent their creation.
986+
if (typeof target !== 'string' || !isAbsolute(toPathIfFileURL(target))) {
987+
throw new ERR_ACCESS_DENIED('relative symbolic link target');
988+
}
989+
}
990+
977991
target = getValidatedPath(target, 'target');
978992
path = getValidatedPath(path);
979993
return await PromisePrototypeThen(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Flags: --experimental-permission --allow-fs-read=* --allow-fs-write=*
2+
'use strict';
3+
4+
const common = require('../common');
5+
common.skipIfWorker();
6+
7+
const assert = require('assert');
8+
const { symlinkSync, symlink, promises: { symlink: symlinkAsync } } = require('fs');
9+
10+
const error = {
11+
code: 'ERR_ACCESS_DENIED',
12+
message: /relative symbolic link target/,
13+
};
14+
15+
for (const targetString of ['a', './b/c', '../d', 'e/../f', 'C:drive-relative', 'ntfs:alternate']) {
16+
for (const target of [targetString, Buffer.from(targetString)]) {
17+
for (const path of [__filename, __dirname, process.execPath]) {
18+
assert.throws(() => symlinkSync(target, path), error);
19+
symlink(target, path, common.mustCall((err) => {
20+
assert(err);
21+
assert.strictEqual(err.code, error.code);
22+
assert.match(err.message, error.message);
23+
}));
24+
assert.rejects(() => symlinkAsync(target, path), error).then(common.mustCall());
25+
}
26+
}
27+
}

0 commit comments

Comments
 (0)