| // Generated by //js/private/node-bootstrap:compile | |
| "use strict"; | |
| /** | |
| * @license | |
| * Copyright 2019 The Bazel Authors. All rights reserved. | |
| * | |
| * Licensed under the Apache License, Version 2.0 (the "License"); | |
| * you may not use this file except in compliance with the License. | |
| * | |
| * You may obtain a copy of the License at | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, software | |
| * distributed under the License is distributed on an "AS IS" BASIS, | |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| * See the License for the specific language governing permissions and | |
| * limitations under the License. | |
| */ | |
| var __asyncValues = (this && this.__asyncValues) || function (o) { | |
| if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined."); | |
| var m = o[Symbol.asyncIterator], i; | |
| return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i); | |
| function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; } | |
| function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); } | |
| }; | |
| var __await = (this && this.__await) || function (v) { return this instanceof __await ? (this.v = v, this) : new __await(v); } | |
| var __asyncGenerator = (this && this.__asyncGenerator) || function (thisArg, _arguments, generator) { | |
| if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined."); | |
| var g = generator.apply(thisArg, _arguments || []), i, q = []; | |
| return i = Object.create((typeof AsyncIterator === "function" ? AsyncIterator : Object).prototype), verb("next"), verb("throw"), verb("return", awaitReturn), i[Symbol.asyncIterator] = function () { return this; }, i; | |
| function awaitReturn(f) { return function (v) { return Promise.resolve(v).then(f, reject); }; } | |
| function verb(n, f) { if (g[n]) { i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; if (f) i[n] = f(i[n]); } } | |
| function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } } | |
| function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); } | |
| function fulfill(value) { resume("next", value); } | |
| function reject(value) { resume("throw", value); } | |
| function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); } | |
| }; | |
| Object.defineProperty(exports, "__esModule", { value: true }); | |
| exports.patcher = patcher; | |
| exports.isSubPath = isSubPath; | |
| exports.escapeFunction = escapeFunction; | |
| const esmModule = require("node:module"); | |
| const path = require("node:path"); | |
| const url = require("node:url"); | |
| const util = require("node:util"); | |
| // using require here on purpose so we can override methods with any | |
| // also even though imports are mutable in typescript the cognitive dissonance is too high because | |
| // es modules | |
| const fs = require('node:fs'); | |
| const HOP_NON_LINK = Symbol.for('HOP NON LINK'); | |
| const HOP_NOT_FOUND = Symbol.for('HOP NOT FOUND'); | |
| const PATCHED_FS_METHODS = [ | |
| 'lstat', | |
| 'lstatSync', | |
| 'realpath', | |
| 'realpathSync', | |
| 'readlink', | |
| 'readlinkSync', | |
| 'readdir', | |
| 'readdirSync', | |
| 'opendir', | |
| ]; | |
| /** | |
| * Function that patches the `fs` module to not escape the given roots. | |
| * @returns a function to undo the patches. | |
| */ | |
| function patcher(roots) { | |
| if (fs._unpatched) { | |
| throw new Error('FS is already patched.'); | |
| } | |
| // Make the original version of the library available for when access to the | |
| // unguarded file system is necessary, such as the esbuild plugin that | |
| // protects against sandbox escaping that occurs through module resolution | |
| // in the Go binary. See | |
| // https://github.com/aspect-build/rules_esbuild/issues/58. | |
| fs._unpatched = PATCHED_FS_METHODS.reduce((obj, method) => { | |
| obj[method] = fs[method]; | |
| return obj; | |
| }, {}); | |
| roots = roots || []; | |
| roots = roots.filter((root) => fs.existsSync(root)); | |
| if (!roots.length) { | |
| if (process.env.VERBOSE_LOGS) { | |
| console.error('fs patcher called without any valid root paths ' + __filename); | |
| } | |
| return function () { }; | |
| } | |
| const origStat = fs.stat.bind(fs); | |
| const origStatSync = fs.statSync.bind(fs); | |
| const origLstat = fs.lstat.bind(fs); | |
| const origLstatSync = fs.lstatSync.bind(fs); | |
| const origReaddir = fs.readdir.bind(fs); | |
| const origReaddirSync = fs.readdirSync.bind(fs); | |
| const origReadlink = fs.readlink.bind(fs); | |
| const origReadlinkSync = fs.readlinkSync.bind(fs); | |
| const origRealpath = fs.realpath.bind(fs); | |
| const origRealpathNative = fs.realpath | |
| .native; | |
| const origRealpathSync = fs.realpathSync.bind(fs); | |
| const origRealpathSyncNative = fs.realpathSync | |
| .native; | |
| const { canEscape, isEscape } = escapeFunction(roots); | |
| const rootMappings = roots | |
| .map((root) => { | |
| const lexical = path.resolve(root); | |
| let real = lexical; | |
| // A configured root need not exist on disk when the patch is | |
| // registered (or may be inaccessible); when it can't be realpath'd | |
| // we keep real == lexical, i.e. treat the root as not-symlinked so | |
| // the remapping below is a no-op. | |
| try { | |
| real = origRealpathSyncNative(lexical); | |
| } | |
| catch (_a) { } | |
| return { lexical, real }; | |
| }) | |
| .sort((a, b) => b.lexical.length - a.lexical.length); | |
| // Resolve a relative symlink `linkTarget` against the REAL location of the | |
| // link's parent directory. `resolved` is an absolute path whose final | |
| // component is a symlink we just read successfully, but the path we were | |
| // queried through may be an alias of a different depth than the real dir | |
| // (e.g. a pnpm/node_modules alias); joining `linkTarget` onto the *lexical* | |
| // parent would then land at the wrong absolute path. | |
| function resolveTargetAgainstRealParent(resolved, linkTarget) { | |
| if (path.isAbsolute(linkTarget)) | |
| return path.resolve(linkTarget); | |
| const linkDir = origRealpathSyncNative(path.dirname(resolved)); | |
| const target = path.resolve(linkDir, linkTarget); | |
| // Project only targets that remain inside a symlinked root. Resolving | |
| // relative escapes after projecting the parent would change their depth. | |
| for (const root of rootMappings) { | |
| if (isSubPath(root.lexical, resolved) && | |
| isSubPath(root.real, target)) { | |
| return path.resolve(root.lexical, path.relative(root.real, target)); | |
| } | |
| } | |
| return target; | |
| } | |
| // A relative path with the same shape is only a valid sandbox remapping | |
| // when it leads to the original target. Otherwise a walker could follow an | |
| // absent or unrelated file in the sandbox. | |
| function hasSameTarget(original, mapped) { | |
| try { | |
| return (origRealpathSyncNative(original) === | |
| origRealpathSyncNative(mapped)); | |
| } | |
| catch (_a) { | |
| return false; | |
| } | |
| } | |
| // ========================================================================= | |
| // fs.lstat | |
| // ========================================================================= | |
| fs.lstat = function lstat(...args) { | |
| // preserve error when calling function without required callback | |
| if (typeof args[args.length - 1] !== 'function') { | |
| return origLstat(...args); | |
| } | |
| const cb = once(args[args.length - 1]); | |
| // override the callback | |
| args[args.length - 1] = function lstatCb(err, stats) { | |
| if (err) | |
| return cb(err); | |
| if (!stats.isSymbolicLink()) { | |
| // the file is not a symbolic link so there is nothing more to do | |
| return cb(null, stats); | |
| } | |
| args[0] = resolvePathLike(args[0]); | |
| if (!canEscape(args[0])) { | |
| // the file can not escaped the sandbox so there is nothing more to do | |
| return cb(null, stats); | |
| } | |
| return readlink(args[0], guardedReadLinkCb); | |
| function guardedReadLinkCb(err) { | |
| if (!err) { | |
| // The final component is a visible link in the guarded filesystem. | |
| return cb(null, stats); | |
| } | |
| if (err.code === 'ENOENT') | |
| return cb(null, stats); | |
| if (err.code !== 'EINVAL') | |
| return cb(err); | |
| // readlink hides this link; return its target's stats, preserving options. | |
| args[args.length - 1] = function statCb(err, targetStats) { | |
| if ((err === null || err === void 0 ? void 0 : err.code) === 'ENOENT') | |
| return cb(null, stats); | |
| if (err) | |
| return cb(err); | |
| return cb(null, targetStats !== null && targetStats !== void 0 ? targetStats : stats); | |
| }; | |
| return origStat(...args); | |
| } | |
| }; | |
| origLstat(...args); | |
| }; | |
| fs.lstatSync = function lstatSync(...args) { | |
| var _a; | |
| const stats = origLstatSync(...args); | |
| if (!(stats === null || stats === void 0 ? void 0 : stats.isSymbolicLink())) { | |
| // the file is not a symbolic link so there is nothing more to do | |
| return stats; | |
| } | |
| args[0] = resolvePathLike(args[0]); | |
| if (!canEscape(args[0])) { | |
| // the file can not escaped the sandbox so there is nothing more to do | |
| return stats; | |
| } | |
| try { | |
| readlinkSync(args[0]); | |
| return stats; | |
| } | |
| catch (err) { | |
| if (err.code === 'ENOENT') | |
| return stats; | |
| if (err.code !== 'EINVAL') | |
| throw err; | |
| } | |
| try { | |
| return (_a = origStatSync(...args)) !== null && _a !== void 0 ? _a : stats; | |
| } | |
| catch (err) { | |
| if (err.code === 'ENOENT') { | |
| // broken link so there is nothing more to do | |
| return stats; | |
| } | |
| throw err; | |
| } | |
| }; | |
| // ========================================================================= | |
| // fs.realpath | |
| // ========================================================================= | |
| fs.realpath = function realpath(...args) { | |
| // preserve error when calling function without required callback | |
| if (typeof args[args.length - 1] !== 'function') { | |
| return origRealpath(...args); | |
| } | |
| const cb = once(args[args.length - 1]); | |
| args[args.length - 1] = function realpathCb(err, str) { | |
| if (err) | |
| return cb(err); | |
| const escapedRoot = isEscape(args[0], str); | |
| if (escapedRoot) { | |
| return guardedRealPath(args[0], cb, [escapedRoot]); | |
| } | |
| else { | |
| return cb(null, str); | |
| } | |
| }; | |
| origRealpath(...args); | |
| }; | |
| fs.realpath.native = function realpath_native(...args) { | |
| // preserve error when calling function without required callback | |
| if (typeof args[args.length - 1] !== 'function') { | |
| return origRealpathNative(...args); | |
| } | |
| const cb = once(args[args.length - 1]); | |
| args[args.length - 1] = function nativeCb(err, str) { | |
| if (err) | |
| return cb(err); | |
| const escapedRoot = isEscape(args[0], str); | |
| if (escapedRoot) { | |
| return guardedRealPath(args[0], cb, [escapedRoot]); | |
| } | |
| else { | |
| return cb(null, str); | |
| } | |
| }; | |
| origRealpathNative(...args); | |
| }; | |
| fs.realpathSync = function realpathSync(...args) { | |
| const str = origRealpathSync(...args); | |
| const escapedRoot = isEscape(args[0], str); | |
| if (escapedRoot) { | |
| return guardedRealPathSync(args[0], [escapedRoot]); | |
| } | |
| return str; | |
| }; | |
| fs.realpathSync.native = function native_realpathSync(...args) { | |
| const str = origRealpathSyncNative(...args); | |
| const escapedRoot = isEscape(args[0], str); | |
| if (escapedRoot) { | |
| return guardedRealPathSync(args[0], [escapedRoot]); | |
| } | |
| return str; | |
| }; | |
| // ========================================================================= | |
| // fs.readlink | |
| // ========================================================================= | |
| fs.readlink = readlink; | |
| function readlink(...args) { | |
| // preserve error when calling function without required callback | |
| if (typeof args[args.length - 1] !== 'function') { | |
| return origReadlink(...args); | |
| } | |
| const cb = once(args[args.length - 1]); | |
| args[args.length - 1] = function readlinkCb(err, p) { | |
| if (err) | |
| return cb(err); | |
| const resolved = resolvePathLike(args[0]); | |
| const linkTarget = p; | |
| let targetAbs; | |
| try { | |
| targetAbs = resolveTargetAgainstRealParent(resolved, linkTarget); | |
| } | |
| catch (error) { | |
| return cb(error); | |
| } | |
| const escapedRoot = isEscape(resolved, targetAbs); | |
| if (escapedRoot) { | |
| const escapedRoots = [escapedRoot]; | |
| return nextHop(targetAbs, readlinkNextHopCb); | |
| function readlinkNextHopCb(next) { | |
| if (!next) { | |
| if (next == undefined) { | |
| // The escape from the root is not mappable back into the root; throw EINVAL | |
| return cb(enoent('readlink', args[0])); | |
| } | |
| else { | |
| // The escape from the root is not mappable back into the root; throw EINVAL | |
| return cb(einval('readlink', args[0])); | |
| } | |
| } | |
| const r = path.resolve(path.dirname(resolved), path.relative(path.dirname(targetAbs), next)); | |
| if (r != resolved && | |
| !isEscape(resolved, r, escapedRoots) && | |
| hasSameTarget(resolved, r)) { | |
| if (path.isAbsolute(linkTarget)) { | |
| return cb(null, r); | |
| } | |
| const rel = path.relative(path.dirname(resolved), r); | |
| return cb(null, rel || '.'); | |
| } | |
| // The escape from the root is not mappable back into the root; throw EINVAL | |
| return cb(einval('readlink', args[0])); | |
| } | |
| } | |
| else { | |
| return cb(null, linkTarget); | |
| } | |
| }; | |
| origReadlink(...args); | |
| } | |
| fs.readlinkSync = readlinkSync; | |
| function readlinkSync(...args) { | |
| const resolved = resolvePathLike(args[0]); | |
| const linkTarget = origReadlinkSync(...args); | |
| const targetAbs = resolveTargetAgainstRealParent(resolved, linkTarget); | |
| const escapedRoot = isEscape(resolved, targetAbs); | |
| if (escapedRoot) { | |
| const next = nextHopSync(targetAbs); | |
| if (!next) { | |
| if (next == undefined) { | |
| // The escape from the root is not mappable back into the root; throw EINVAL | |
| throw enoent('readlink', args[0]); | |
| } | |
| else { | |
| // The escape from the root is not mappable back into the root; throw EINVAL | |
| throw einval('readlink', args[0]); | |
| } | |
| } | |
| const r = path.resolve(path.dirname(resolved), path.relative(path.dirname(targetAbs), next)); | |
| if (r != resolved && | |
| !isEscape(resolved, r, [escapedRoot]) && | |
| hasSameTarget(resolved, r)) { | |
| if (path.isAbsolute(linkTarget)) { | |
| return r; | |
| } | |
| const rel = path.relative(path.dirname(resolved), r); | |
| return rel || '.'; | |
| } | |
| // The escape from the root is not mappable back into the root; throw EINVAL | |
| throw einval('readlink', args[0]); | |
| } | |
| return linkTarget; | |
| } | |
| // ========================================================================= | |
| // fs.readdir | |
| // ========================================================================= | |
| fs.readdir = function readdir(...args) { | |
| // preserve error when calling function without required callback | |
| if (typeof args[args.length - 1] !== 'function') { | |
| return origReaddir(...args); | |
| } | |
| const cb = once(args[args.length - 1]); | |
| const p = resolvePathLike(args[0]); | |
| args[args.length - 1] = function readdirCb(err, result) { | |
| if (err) | |
| return cb(err); | |
| // user requested withFileTypes | |
| if (result[0] && result[0].isSymbolicLink) { | |
| Promise.all(result.map((v) => handleDirent(p, v))) | |
| .then(() => { | |
| cb(null, result); | |
| }) | |
| .catch((err) => { | |
| cb(err); | |
| }); | |
| } | |
| else { | |
| // string array return for readdir. | |
| cb(null, result); | |
| } | |
| }; | |
| origReaddir(...args); | |
| }; | |
| fs.readdirSync = function readdirSync(...args) { | |
| const res = origReaddirSync(...args); | |
| const p = resolvePathLike(args[0]); | |
| res.forEach((v) => { | |
| handleDirentSync(p, v); | |
| }); | |
| return res; | |
| }; | |
| // ========================================================================= | |
| // fs.opendir | |
| // ========================================================================= | |
| if (fs.opendir) { | |
| const origOpendir = fs.opendir.bind(fs); | |
| fs.opendir = function opendir(...args) { | |
| // if this is not a function opendir should throw an error. | |
| // we call it so we don't have to throw a mock | |
| if (typeof args[args.length - 1] === 'function') { | |
| const cb = once(args[args.length - 1]); | |
| args[args.length - 1] = async function opendirCb(err, dir) { | |
| try { | |
| cb(err, err ? undefined : handleDir(dir)); | |
| } | |
| catch (err) { | |
| cb(err); | |
| } | |
| }; | |
| origOpendir(...args); | |
| } | |
| else { | |
| return origOpendir(...args).then(handleDir); | |
| } | |
| }; | |
| } | |
| if (fs.opendirSync) { | |
| const origOpendirSync = fs.opendirSync.bind(fs); | |
| fs.opendirSync = function opendirSync(...args) { | |
| const dir = origOpendirSync(...args); | |
| return handleDir(dir); | |
| }; | |
| } | |
| // ========================================================================= | |
| // fs.promises | |
| // ========================================================================= | |
| /** | |
| * patch fs.promises here. | |
| * | |
| * this requires a light touch because if we trigger the getter on older nodejs versions | |
| * it will log an experimental warning to stderr | |
| * | |
| * `(node:62945) ExperimentalWarning: The fs.promises API is experimental` | |
| * | |
| * this api is available as experimental without a flag so users can access it at any time. | |
| */ | |
| const promisePropertyDescriptor = Object.getOwnPropertyDescriptor(fs, 'promises'); | |
| let unpatchPromises; | |
| if (promisePropertyDescriptor) { | |
| const promises = {}; | |
| promises.lstat = util.promisify(fs.lstat); | |
| // NOTE: node core uses the newer realpath function fs.promises.native instead of fs.realPath | |
| promises.realpath = util.promisify(fs.realpath.native); | |
| promises.readlink = util.promisify(fs.readlink); | |
| promises.readdir = util.promisify(fs.readdir); | |
| if (fs.opendir) | |
| promises.opendir = util.promisify(fs.opendir); | |
| // handle experimental api warnings. | |
| // only applies to version of node where promises is a getter property. | |
| if (promisePropertyDescriptor.get) { | |
| const oldGetter = promisePropertyDescriptor.get.bind(fs); | |
| const cachedPromises = {}; | |
| function promisePropertyGetter() { | |
| const _promises = oldGetter(); | |
| Object.assign(cachedPromises, _promises, promises); | |
| return cachedPromises; | |
| } | |
| Object.defineProperty(fs, 'promises', Object.assign(Object.create(promisePropertyDescriptor), { | |
| get: promisePropertyGetter, | |
| })); | |
| unpatchPromises = function unpatchFsPromises() { | |
| Object.defineProperty(fs, 'promises', promisePropertyDescriptor); | |
| }; | |
| } | |
| else { | |
| const unpatchedPromises = Object.keys(promises).reduce((obj, method) => { | |
| obj[method] = fs.promises[method]; | |
| return obj; | |
| }, Object.create(fs.promises)); | |
| // api can be patched directly | |
| Object.assign(fs.promises, promises); | |
| unpatchPromises = function unpatchFsPromises() { | |
| Object.assign(fs.promises, unpatchedPromises); | |
| }; | |
| } | |
| } | |
| // ========================================================================= | |
| // helper functions for dirs | |
| // ========================================================================= | |
| function handleDir(dir) { | |
| const p = path.resolve(dir.path); | |
| const origIterator = dir[Symbol.asyncIterator].bind(dir); | |
| dir[Symbol.asyncIterator] = function () { | |
| return __asyncGenerator(this, arguments, function* () { | |
| var _a, e_1, _b, _c; | |
| try { | |
| for (var _d = true, _f = __asyncValues(origIterator()), _g; _g = yield __await(_f.next()), _a = _g.done, !_a; _d = true) { | |
| _c = _g.value; | |
| _d = false; | |
| const entry = _c; | |
| yield __await(handleDirent(p, entry)); | |
| yield yield __await(entry); | |
| } | |
| } | |
| catch (e_1_1) { e_1 = { error: e_1_1 }; } | |
| finally { | |
| try { | |
| if (!_d && !_a && (_b = _f.return)) yield __await(_b.call(_f)); | |
| } | |
| finally { if (e_1) throw e_1.error; } | |
| } | |
| }); | |
| }; | |
| const origRead = dir.read.bind(dir); | |
| dir.read = function readWrapper() { | |
| if (typeof arguments[0] === 'function') { | |
| return handleDirReadCallback(arguments[0]); | |
| } | |
| else { | |
| return handleDirReadPromise(); | |
| } | |
| }; | |
| // read(cb: (err: NodeJS.ErrnoException | null, dirEnt: Dirent | null) => void): void; | |
| function handleDirReadCallback(cb) { | |
| origRead(function handleDirReadCb(err, entry) { | |
| if (err) | |
| return cb(err, null); | |
| handleDirent(p, entry).then(() => { | |
| cb(null, entry); | |
| }, (err) => cb(err, null)); | |
| }); | |
| } | |
| // read(): Promise<Dirent | null>; | |
| async function handleDirReadPromise() { | |
| const entry = await origRead(); | |
| if (entry) { | |
| await handleDirent(p, entry); | |
| } | |
| return entry; | |
| } | |
| const origReadSync = dir.readSync.bind(dir); | |
| dir.readSync = function handleDirReadSync() { | |
| return handleDirentSync(p, origReadSync()); | |
| }; | |
| return dir; | |
| } | |
| async function handleDirent(p, v) { | |
| if (!v.isSymbolicLink()) { | |
| return v; | |
| } | |
| const entryName = typeof v.name === 'string' ? v.name : v.name.toString(); | |
| const f = path.resolve(p, entryName); | |
| return new Promise(function handleDirentExecutor(resolve, reject) { | |
| return guardedReadLink(f, handleDirentReadLinkCb); | |
| function handleDirentReadLinkCb(str) { | |
| if (f != str) { | |
| return resolve(v); | |
| } | |
| // There are no hops so we should hide the fact that the file is a symlink | |
| v.isSymbolicLink = () => false; | |
| origRealpath(f, function handleDirentRealpathCb(err, str) { | |
| if (err) { | |
| return reject(err); | |
| } | |
| fs.stat(str, function handleDirentStatCb(err, stat) { | |
| if (err) { | |
| return reject(err); | |
| } | |
| patchDirent(v, stat); | |
| resolve(v); | |
| }); | |
| }); | |
| } | |
| }); | |
| } | |
| function handleDirentSync(p, v) { | |
| if (v && v.isSymbolicLink) { | |
| if (v.isSymbolicLink()) { | |
| const f = path.resolve(p, v.name); | |
| if (f == guardedReadLinkSync(f)) { | |
| // There are no hops so we should hide the fact that the file is a symlink | |
| v.isSymbolicLink = () => false; | |
| const stat = fs.statSync(origRealpathSync(f)); | |
| patchDirent(v, stat); | |
| } | |
| } | |
| } | |
| return v; | |
| } | |
| function nextHop(loc, cb) { | |
| let nested = ''; | |
| let maybe = loc; | |
| let escapedHop = false; | |
| readHopLink(maybe, function readNextHop(link) { | |
| if (link === HOP_NOT_FOUND) { | |
| return cb(undefined); | |
| } | |
| if (link !== HOP_NON_LINK) { | |
| if (nested) { | |
| link = link + path.sep + nested; | |
| } | |
| if (!isEscape(loc, link)) { | |
| return cb(link); | |
| } | |
| if (!escapedHop) { | |
| escapedHop = link; | |
| } | |
| } | |
| const dirname = path.dirname(maybe); | |
| if (!dirname || | |
| dirname == maybe || | |
| dirname == '.' || | |
| dirname == '/') { | |
| // not a link | |
| return cb(escapedHop); | |
| } | |
| nested = path.basename(maybe) + (nested ? path.sep + nested : ''); | |
| maybe = dirname; | |
| readHopLink(maybe, readNextHop); | |
| }); | |
| } | |
| const symlinkNoThrow = Object.freeze({ | |
| throwIfNoEntry: false, | |
| }); | |
| const hopLinkCache = Object.create(null); | |
| function readHopLinkSync(p) { | |
| if (hopLinkCache[p]) { | |
| return hopLinkCache[p]; | |
| } | |
| let link; | |
| const pStats = origLstatSync(p, symlinkNoThrow); | |
| if (!pStats) { | |
| link = HOP_NOT_FOUND; | |
| } | |
| else if (pStats.isSymbolicLink()) { | |
| link = origReadlinkSync(p); | |
| if (link) { | |
| if (!path.isAbsolute(link)) { | |
| link = path.resolve(path.dirname(p), link); | |
| } | |
| } | |
| else { | |
| link = HOP_NON_LINK; | |
| } | |
| } | |
| else { | |
| link = HOP_NON_LINK; | |
| } | |
| hopLinkCache[p] = link; | |
| return link; | |
| } | |
| function readHopLink(p, cb) { | |
| if (hopLinkCache[p]) { | |
| return cb(hopLinkCache[p]); | |
| } | |
| origReadlink(p, function readHopLinkCb(err, link) { | |
| if (err) { | |
| let result; | |
| if (err.code === 'ENOENT') { | |
| // file does not exist | |
| result = HOP_NOT_FOUND; | |
| } | |
| else { | |
| result = HOP_NON_LINK; | |
| } | |
| hopLinkCache[p] = result; | |
| return cb(result); | |
| } | |
| if (link === undefined) { | |
| hopLinkCache[p] = HOP_NON_LINK; | |
| return cb(HOP_NON_LINK); | |
| } | |
| if (!path.isAbsolute(link)) { | |
| link = path.resolve(path.dirname(p), link); | |
| } | |
| hopLinkCache[p] = link; | |
| cb(link); | |
| }); | |
| } | |
| function nextHopSync(loc) { | |
| let nested = ''; | |
| let maybe = loc; | |
| let escapedHop = false; | |
| for (;;) { | |
| let link = readHopLinkSync(maybe); | |
| if (link === HOP_NOT_FOUND) { | |
| return false; | |
| } | |
| if (link !== HOP_NON_LINK) { | |
| if (nested) { | |
| link = link + path.sep + nested; | |
| } | |
| if (!isEscape(loc, link)) { | |
| return link; | |
| } | |
| if (!escapedHop) { | |
| escapedHop = link; | |
| } | |
| } | |
| const dirname = path.dirname(maybe); | |
| if (!dirname || | |
| dirname == maybe || | |
| dirname == '.' || | |
| dirname == '/') { | |
| // not a link | |
| return escapedHop; | |
| } | |
| nested = path.basename(maybe) + (nested ? path.sep + nested : ''); | |
| maybe = dirname; | |
| } | |
| } | |
| function guardedReadLink(loc, cb) { | |
| nextHop(loc, guardedReadLinkHopCb); | |
| function guardedReadLinkHopCb(next) { | |
| if (!next) { | |
| // we're no longer hopping but we haven't escaped; | |
| // something funky happened in the filesystem | |
| return cb(loc); | |
| } | |
| if (isEscape(loc, next)) { | |
| // this hop takes us out of the guard | |
| return cb(loc); | |
| } | |
| return cb(next); | |
| } | |
| } | |
| function guardedReadLinkSync(loc) { | |
| const next = nextHopSync(loc); | |
| if (!next) { | |
| // we're no longer hopping but we haven't escaped; | |
| // something funky happened in the filesystem | |
| return loc; | |
| } | |
| if (isEscape(loc, next)) { | |
| // this hop takes us out of the guard | |
| return loc; | |
| } | |
| return next; | |
| } | |
| function guardedRealPath(start, cb, escapedRoots) { | |
| // stringifyPathLike() to handle the "undefined" case (matches behavior as fs.realpath) | |
| oneHop(stringifyPathLike(start), cb); | |
| function oneHop(loc, cb) { | |
| nextHop(loc, function guardedRealPathHopCb(next) { | |
| if (!next) { | |
| // we're no longer hopping but we haven't escaped | |
| return fs.exists(loc, function guardedRealPathExistsCb(e) { | |
| if (e) { | |
| // we hit a real file within the guard and can go no further | |
| return cb(null, loc); | |
| } | |
| else { | |
| // something funky happened in the filesystem | |
| return cb(enoent('realpath', start), undefined); | |
| } | |
| }); | |
| } | |
| if (isEscape(loc, next, escapedRoots)) { | |
| // this hop takes us out of the guard | |
| return cb(null, loc); | |
| } | |
| oneHop(next, cb); | |
| }); | |
| } | |
| } | |
| function guardedRealPathSync(start, escapedRoots) { | |
| // stringifyPathLike() to handle the "undefined" case (matches behavior as fs.realpathSync) | |
| for (let loc = stringifyPathLike(start), next;; loc = next) { | |
| next = nextHopSync(loc); | |
| if (!next) { | |
| // we're no longer hopping but we haven't escaped | |
| if (fs.existsSync(loc)) { | |
| // we hit a real file within the guard and can go no further | |
| return loc; | |
| } | |
| else { | |
| // something funky happened in the filesystem; throw ENOENT | |
| throw enoent('realpath', start); | |
| } | |
| } | |
| if (isEscape(loc, next, escapedRoots)) { | |
| // this hop takes us out of the guard | |
| return loc; | |
| } | |
| } | |
| } | |
| // Sync the esm modules to use the now patched fs cjs module. | |
| // See: https://nodejs.org/api/esm.html#builtin-modules | |
| esmModule.syncBuiltinESMExports(); | |
| return function revertPatch() { | |
| Object.assign(fs, fs._unpatched); | |
| delete fs._unpatched; | |
| if (unpatchPromises) { | |
| unpatchPromises(); | |
| } | |
| // Re-sync the esm modules to revert to the unpatched module. | |
| esmModule.syncBuiltinESMExports(); | |
| }; | |
| } | |
| // ========================================================================= | |
| // generic helper functions | |
| // ========================================================================= | |
| function isSubPath(parent, child) { | |
| return (parent === child || | |
| (child[parent.length] === path.sep && child.startsWith(parent))); | |
| } | |
| function stringifyPathLike(p) { | |
| if (p instanceof URL) { | |
| return url.fileURLToPath(p); | |
| } | |
| else { | |
| return String(p); | |
| } | |
| } | |
| function resolvePathLike(p) { | |
| return path.resolve(stringifyPathLike(p)); | |
| } | |
| function normalizePathLike(p) { | |
| const s = stringifyPathLike(p); | |
| // TODO: are URLs always absolute? | |
| if (!path.isAbsolute(s)) { | |
| return path.resolve(s); | |
| } | |
| else { | |
| return path.normalize(s); | |
| } | |
| } | |
| function escapeFunction(_roots) { | |
| // Ensure roots are always absolute. | |
| // Sort to ensure escaping multiple roots chooses the longest one. | |
| const defaultRoots = _roots | |
| .map((root) => path.resolve(root)) | |
| .sort((a, b) => b.length - a.length); | |
| function fs_isEscape(linkPath, linkTarget, roots = defaultRoots) { | |
| // linkPath is the path of the symlink file itself | |
| // linkTarget is a path that the symlink points to one or more hops away | |
| // linkTarget must already be normalized | |
| linkPath = normalizePathLike(linkPath); | |
| for (const root of roots) { | |
| // If the link is in the root check if the realPath has escaped | |
| if (isSubPath(root, linkPath) && !isSubPath(root, linkTarget)) { | |
| return root; | |
| } | |
| } | |
| return false; | |
| } | |
| function fs_canEscape(maybeLinkPath, roots = defaultRoots) { | |
| // maybeLinkPath is the path which may be a symlink | |
| // maybeLinkPath must already be normalized | |
| for (const root of roots) { | |
| // If the link is in the root check if the realPath has escaped | |
| if (isSubPath(root, maybeLinkPath)) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| return { | |
| isEscape: fs_isEscape, | |
| canEscape: fs_canEscape, | |
| }; | |
| } | |
| function once(fn) { | |
| let called = false; | |
| return function callOnce(...args) { | |
| if (called) | |
| return; | |
| called = true; | |
| let err = false; | |
| try { | |
| fn(...args); | |
| } | |
| catch (_e) { | |
| err = _e; | |
| } | |
| // blow the stack to make sure this doesn't fall into any unresolved promise contexts | |
| if (err) { | |
| setImmediate(() => { | |
| throw err; | |
| }); | |
| } | |
| }; | |
| } | |
| function patchDirent(dirent, stat) { | |
| // add all stat is methods to Dirent instances with their result. | |
| for (const i in stat) { | |
| if (i.startsWith('is') && typeof stat[i] === 'function') { | |
| // | |
| const result = stat[i](); | |
| if (result) | |
| dirent[i] = () => true; | |
| else | |
| dirent[i] = () => false; | |
| } | |
| } | |
| } | |
| function enoent(s, p) { | |
| let err = new Error(`ENOENT: no such file or directory, ${s} '${p}'`); | |
| err.errno = -2; | |
| err.syscall = s; | |
| err.code = 'ENOENT'; | |
| err.path = p; | |
| return err; | |
| } | |
| function einval(s, p) { | |
| let err = new Error(`EINVAL: invalid argument, ${s} '${p}'`); | |
| err.errno = -22; | |
| err.syscall = s; | |
| err.code = 'EINVAL'; | |
| err.path = p; | |
| return err; | |
| } |