diff --git a/lib/fs.js b/lib/fs.js index 34c58e22ad54a4..4f4246d19b56c7 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 { } = require('internal/validators'); const { readFileSyncUtf8 } = require('internal/fs/read/utf8'); +const permission = require('internal/process/permission'); + let truncateWarn = true; let fs; @@ -1762,6 +1766,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 (!isAbsolute(toPathIfFileURL(target))) { + callback(new ERR_ACCESS_DENIED('relative symbolic link target')); + return; + } + } + target = getValidatedPath(target, 'target'); path = getValidatedPath(path); @@ -1818,6 +1831,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 (!isAbsolute(toPathIfFileURL(target))) { + throw new ERR_ACCESS_DENIED('relative symbolic link target'); + } + } + target = getValidatedPath(target, 'target'); path = getValidatedPath(path); const flags = stringToSymlinkType(type); diff --git a/lib/internal/fs/promises.js b/lib/internal/fs/promises.js index 8f56baf77505f1..0f934ab24f198b 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, @@ -84,6 +85,8 @@ const { validateString, } = require('internal/validators'); const pathModule = require('path'); +const { isAbsolute } = pathModule; +const { toPathIfFileURL } = require('internal/url'); const { kEmptyObject, lazyDOMException, @@ -96,6 +99,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'); @@ -877,6 +882,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 (!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..2fef2898939803 --- /dev/null +++ b/test/parallel/test-permission-fs-symlink-relative.js @@ -0,0 +1,25 @@ +// 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 target of ['a', './b/c', '../d', 'e/../f', 'C:drive-relative', 'ntfs:alternate']) { + 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); + } +}