diff --git a/lib/fs.js b/lib/fs.js index 7edf3232ad15dc..bce9d38026169c 100644 --- a/lib/fs.js +++ b/lib/fs.js @@ -58,6 +58,7 @@ const { } = constants; const pathModule = require('path'); +const { isAbsolute } = pathModule; const { isArrayBufferView } = require('internal/util/types'); const binding = internalBinding('fs'); @@ -68,6 +69,7 @@ const { Buffer } = require('buffer'); const { aggregateTwoErrors, codes: { + ERR_ACCESS_DENIED, ERR_FS_FILE_TOO_LARGE, ERR_INVALID_ARG_VALUE, }, @@ -143,6 +145,8 @@ const { kValidateObjectAllowNullable, } = require('internal/validators'); +const permission = require('internal/process/permission'); + let truncateWarn = true; let fs; @@ -1740,6 +1744,15 @@ function symlink(target, path, type_, callback_) { const type = (typeof type_ === 'string' ? type_ : null); const callback = makeCallback(arguments[arguments.length - 1]); + if (permission.isEnabled()) { + // The permission model's security guarantees fall apart in the presence of + // relative symbolic links. Thus, we have to prevent their creation. + if (typeof target !== 'string' || !isAbsolute(toPathIfFileURL(target))) { + callback(new ERR_ACCESS_DENIED('relative symbolic link target')); + return; + } + } + target = getValidatedPath(target, 'target'); path = getValidatedPath(path); @@ -1796,6 +1809,15 @@ function symlinkSync(target, path, type) { type = 'dir'; } } + + if (permission.isEnabled()) { + // The permission model's security guarantees fall apart in the presence of + // relative symbolic links. Thus, we have to prevent their creation. + if (typeof target !== 'string' || !isAbsolute(toPathIfFileURL(target))) { + throw new ERR_ACCESS_DENIED('relative symbolic link target'); + } + } + target = getValidatedPath(target, 'target'); path = getValidatedPath(path); diff --git a/lib/internal/fs/promises.js b/lib/internal/fs/promises.js index 11d230077604bf..11f2b282dd010d 100644 --- a/lib/internal/fs/promises.js +++ b/lib/internal/fs/promises.js @@ -33,6 +33,7 @@ const { Buffer } = require('buffer'); const { codes: { + ERR_ACCESS_DENIED, ERR_FS_FILE_TOO_LARGE, ERR_INVALID_ARG_VALUE, ERR_INVALID_STATE, @@ -85,6 +86,8 @@ const { kValidateObjectAllowNullable, } = require('internal/validators'); const pathModule = require('path'); +const { isAbsolute } = pathModule; +const { toPathIfFileURL } = require('internal/url'); const { kEmptyObject, lazyDOMException, @@ -97,6 +100,8 @@ const nonNativeWatcher = require('internal/fs/recursive_watch'); const { isIterable } = require('internal/streams/utils'); const assert = require('internal/assert'); +const permission = require('internal/process/permission'); + const kHandle = Symbol('kHandle'); const kFd = Symbol('kFd'); const kRefs = Symbol('kRefs'); @@ -883,6 +888,15 @@ async function symlink(target, path, type_) { type = 'file'; } } + + if (permission.isEnabled()) { + // The permission model's security guarantees fall apart in the presence of + // relative symbolic links. Thus, we have to prevent their creation. + if (typeof target !== 'string' || !isAbsolute(toPathIfFileURL(target))) { + throw new ERR_ACCESS_DENIED('relative symbolic link target'); + } + } + target = getValidatedPath(target, 'target'); path = getValidatedPath(path); return binding.symlink(preprocessSymlinkDestination(target, type, path), diff --git a/test/parallel/test-permission-fs-symlink-relative.js b/test/parallel/test-permission-fs-symlink-relative.js new file mode 100644 index 00000000000000..e7cd99ad5f227a --- /dev/null +++ b/test/parallel/test-permission-fs-symlink-relative.js @@ -0,0 +1,27 @@ +// Flags: --experimental-permission --allow-fs-read=* --allow-fs-write=* +'use strict'; + +const common = require('../common'); +common.skipIfWorker(); + +const assert = require('assert'); +const { symlinkSync, symlink, promises: { symlink: symlinkAsync } } = require('fs'); + +const error = { + code: 'ERR_ACCESS_DENIED', + message: /relative symbolic link target/, +}; + +for (const targetString of ['a', './b/c', '../d', 'e/../f', 'C:drive-relative', 'ntfs:alternate']) { + for (const target of [targetString, Buffer.from(targetString)]) { + for (const path of [__filename, __dirname, process.execPath]) { + assert.throws(() => symlinkSync(target, path), error); + symlink(target, path, common.mustCall((err) => { + assert(err); + assert.strictEqual(err.code, error.code); + assert.match(err.message, error.message); + })); + assert.rejects(() => symlinkAsync(target, path), error).then(common.mustCall()); + } + } +}