From 0c166114ce986e41caa3a7dfde2b1bf50bc5b1b3 Mon Sep 17 00:00:00 2001 From: Greg Magolan Date: Mon, 6 Apr 2020 18:51:35 -0700 Subject: [PATCH] fix(builtin): don't allow symlinks to escape or enter bazel managed node_module folders --- internal/node/BUILD.bazel | 2 + internal/node/launcher.sh | 31 +++++ internal/node/node.bzl | 15 ++- internal/node/node_patches.js | 29 ++++- internal/node/test/BUILD.bazel | 35 ++++- internal/node/test/dump_build_env.js | 3 + internal/node/test/env.spec.js | 94 ++++++++++++++ internal/providers/node_runtime_deps_info.bzl | 19 +++ packages/node-patches/register.ts | 5 +- packages/node-patches/src/fs.ts | 25 +++- packages/node-patches/test/fs/escape.ts | 122 +++++++++++++++++- packages/node-patches/test/fs/lstat.ts | 49 +++++-- packages/node-patches/test/fs/opendir.ts | 8 +- packages/node-patches/test/fs/readdir.ts | 4 +- packages/node-patches/test/fs/readlink.ts | 4 +- packages/node-patches/test/fs/realpath.ts | 4 +- 16 files changed, 407 insertions(+), 42 deletions(-) create mode 100644 internal/node/test/dump_build_env.js create mode 100644 internal/node/test/env.spec.js diff --git a/internal/node/BUILD.bazel b/internal/node/BUILD.bazel index 01f1454dd3..c35e61a1b7 100644 --- a/internal/node/BUILD.bazel +++ b/internal/node/BUILD.bazel @@ -46,6 +46,8 @@ filegroup( visibility = ["//:__pkg__"], ) +# To update node_patches.js run: +# bazel run //internal/node:checked_in_node_patches.accept golden_file_test( name = "checked_in_node_patches", actual = "//packages/node-patches:bundle", diff --git a/internal/node/launcher.sh b/internal/node/launcher.sh index 6ad3e28fed..ba149e03d4 100644 --- a/internal/node/launcher.sh +++ b/internal/node/launcher.sh @@ -217,6 +217,37 @@ fi # Bazel always sets the PWD to execroot/my_wksp so we go up one directory. export BAZEL_PATCH_ROOT=$(dirname $PWD) +# Set all bazel managed node_modules directories as guarded so no symlinks may +# escape and no symlinks may enter +if [[ "$PWD" == *"/bazel-out/"* ]]; then + # We in runfiles, find the execroot. + # Look for `bazel-out` which is used to determine the the path to `execroot/my_wksp`. This works in + # all cases including on rbe where the execroot is a path such as `/b/f/w`. For example, when in + # runfiles on rbe, bazel runs the process in a directory such as + # `/b/f/w/bazel-out/k8-fastbuild/bin/path/to/pkg/some_test.sh.runfiles/my_wksp`. From here we can + # determine the execroot `b/f/w` by finding the first instance of bazel-out. + readonly bazel_out="/bazel-out/" + readonly rest=${PWD#*$bazel_out} + readonly index=$(( ${#PWD} - ${#rest} - ${#bazel_out} )) + if [[ ${index} < 0 ]]; then + echo "No 'bazel-out' folder found in path '${PWD}'!" + exit 1 + fi + readonly execroot=${PWD:0:${index}} + export BAZEL_PATCH_GUARDS="${execroot}/node_modules" +else + # We are in execroot, linker node_modules is in the PWD + export BAZEL_PATCH_GUARDS="${PWD}/node_modules" +fi +if [[ -n "${BAZEL_NODE_MODULES_ROOT:-}" ]]; then + if [[ "${BAZEL_NODE_MODULES_ROOT}" != "${BAZEL_WORKSPACE}/node_modules" ]]; then + # If BAZEL_NODE_MODULES_ROOT is set and it is not , add it to the list of bazel patch guards + # Also, add the external/${BAZEL_NODE_MODULES_ROOT} which is the correct path under execroot + # and under runfiles it is the legacy external runfiles path + export BAZEL_PATCH_GUARDS="${BAZEL_PATCH_GUARDS},${BAZEL_PATCH_ROOT}/${BAZEL_NODE_MODULES_ROOT},${PWD}/external/${BAZEL_NODE_MODULES_ROOT}" + fi +fi + # The EXPECTED_EXIT_CODE lets us write bazel tests which assert that # a binary fails to run. Otherwise any failure would make such a test # fail before we could assert that we expected that failure. diff --git a/internal/node/node.bzl b/internal/node/node.bzl index bee2b9edbc..858bfa738d 100644 --- a/internal/node/node.bzl +++ b/internal/node/node.bzl @@ -79,7 +79,7 @@ def _compute_node_modules_root(ctx): ] if f]) return node_modules_root -def _write_require_patch_script(ctx): +def _write_require_patch_script(ctx, node_modules_root): # Generates the JavaScript snippet of module roots mappings, with each entry # in the form: # {module_name: /^mod_name\b/, module_root: 'path/to/mod_name'} @@ -91,8 +91,6 @@ def _write_require_patch_script(ctx): mapping = "{module_name: /^%s\\b/, module_root: '%s'}" % (escaped, mr) module_mappings.append(mapping) - node_modules_root = _compute_node_modules_root(ctx) - ctx.actions.expand_template( template = ctx.file._require_patch_template, output = ctx.outputs.require_patch_script, @@ -175,7 +173,9 @@ def _nodejs_binary_impl(ctx): sources_depsets.append(d.files) sources = depset(transitive = sources_depsets) - _write_require_patch_script(ctx) + node_modules_root = _compute_node_modules_root(ctx) + + _write_require_patch_script(ctx, node_modules_root) _write_loader_script(ctx) # Provide the target name as an environment variable avaiable to all actions for the @@ -190,6 +190,13 @@ def _nodejs_binary_impl(ctx): # runfiles helpers to use. env_vars += "export BAZEL_WORKSPACE=%s\n" % ctx.workspace_name + # if BAZEL_NODE_MODULES_ROOT has not already been set by + # run_node, then set it to the computed value + env_vars += """if [[ -z "${BAZEL_NODE_MODULES_ROOT:-}" ]]; then + export BAZEL_NODE_MODULES_ROOT=%s +fi +""" % node_modules_root + for k in ctx.attr.configuration_env_vars + ctx.attr.default_env_vars: # Check ctx.var first & if env var not in there then check # ctx.configuration.default_shell_env. The former will contain values from --define=FOO=BAR diff --git a/internal/node/node_patches.js b/internal/node/node_patches.js index d7273adcd2..6a1fc0ecac 100644 --- a/internal/node/node_patches.js +++ b/internal/node/node_patches.js @@ -61,9 +61,10 @@ Object.defineProperty(exports, "__esModule", { value: true }); // es modules // tslint:disable-next-line:no-any -exports.patcher = (fs = fs$1, root) => { +exports.patcher = (fs = fs$1, root, guards) => { fs = fs || fs$1; root = root || ''; + guards = guards || []; if (!root) { if (process.env.VERBOSE_LOGS) { console.error('fs patcher called without root path ' + __filename); @@ -83,7 +84,7 @@ exports.patcher = (fs = fs$1, root) => { const origReadlinkSync = fs.readlinkSync.bind(fs); const origReaddir = fs.readdir.bind(fs); const origReaddirSync = fs.readdirSync.bind(fs); - const { isEscape, isOutPath } = exports.escapeFunction(root); + const { isEscape, isOutPath } = exports.escapeFunction(root, guards); // tslint:disable-next-line:no-any fs.lstat = (...args) => { let cb = args.length > 1 ? args[args.length - 1] : undefined; @@ -483,9 +484,10 @@ exports.patcher = (fs = fs$1, root) => { } } }; -exports.escapeFunction = (root) => { - // ensure root is always absolute. +exports.escapeFunction = (root, guards) => { + // ensure root & guards are always absolute. root = path.resolve(root); + guards = guards.map(g => path.resolve(g)); function isEscape(linkTarget, linkPath) { if (!path.isAbsolute(linkPath)) { linkPath = path.resolve(linkPath); @@ -493,17 +495,29 @@ exports.escapeFunction = (root) => { if (!path.isAbsolute(linkTarget)) { linkTarget = path.resolve(linkTarget); } + if (isGuardPath(linkPath) || isGuardPath(linkTarget)) { + // don't escape out of guard paths and don't symlink into guard paths + return true; + } if (root) { if (isOutPath(linkTarget) && !isOutPath(linkPath)) { + // don't escape out of the root return true; } } return false; } + function isGuardPath(str) { + for (const g of guards) { + if (str === g || str.startsWith(g + path.sep)) + return true; + } + return false; + } function isOutPath(str) { return !root || (!str.startsWith(root + path.sep) && str !== root); } - return { isEscape, isOutPath }; + return { isEscape, isGuardPath, isOutPath }; }; function once(fn) { let called = false; @@ -644,12 +658,13 @@ var src_2 = src.subprocess; */ // todo auto detect bazel env vars instead of adding a new one. -const { BAZEL_PATCH_ROOT, NP_SUBPROCESS_BIN_DIR, VERBOSE_LOGS } = process.env; +const { BAZEL_PATCH_ROOT, BAZEL_PATCH_GUARDS, NP_SUBPROCESS_BIN_DIR, VERBOSE_LOGS } = process.env; if (BAZEL_PATCH_ROOT) { + const guards = BAZEL_PATCH_GUARDS ? BAZEL_PATCH_GUARDS.split(',') : []; if (VERBOSE_LOGS) console.error(`bazel node patches enabled. root: ${BAZEL_PATCH_ROOT} symlinks in this directory will not escape`); const fs = fs$1; - src.fs(fs, BAZEL_PATCH_ROOT); + src.fs(fs, BAZEL_PATCH_ROOT, guards); } else if (VERBOSE_LOGS) { console.error(`bazel node patches disabled. set environment BAZEL_PATCH_ROOT`); diff --git a/internal/node/test/BUILD.bazel b/internal/node/test/BUILD.bazel index f2fbb5b04b..3a2fcae006 100644 --- a/internal/node/test/BUILD.bazel +++ b/internal/node/test/BUILD.bazel @@ -70,13 +70,21 @@ nodejs_binary( entry_point = ":module-name.js", ) +jasmine_node_test( + name = "env_test", + srcs = [":env.spec.js"], + data = [ + ":dump_build_env.json", + ":dump_build_env_alt.json", + ], +) + nodejs_test( name = "define_var", configuration_env_vars = [ "SOME_TEST_ENV", "SOME_OTHER_ENV", ], - data = glob(["*.spec.js"]), entry_point = ":define.spec.js", ) @@ -312,6 +320,31 @@ jasmine_node_test( ], ) +nodejs_binary( + name = "dump_build_env", + entry_point = "dump_build_env.js", +) + +nodejs_binary( + name = "dump_build_env_alt", + data = ["@npm//tmp"], + entry_point = "dump_build_env.js", +) + +npm_package_bin( + name = "dump_build_env_json", + outs = ["dump_build_env.json"], + args = ["$@"], + tool = ":dump_build_env", +) + +npm_package_bin( + name = "dump_build_env_alt_json", + outs = ["dump_build_env_alt.json"], + args = ["$@"], + tool = ":dump_build_env_alt", +) + nodejs_binary( name = "test_runfiles_helper", data = [":test_runfiles_helper.golden"], diff --git a/internal/node/test/dump_build_env.js b/internal/node/test/dump_build_env.js new file mode 100644 index 0000000000..f66c04d3c1 --- /dev/null +++ b/internal/node/test/dump_build_env.js @@ -0,0 +1,3 @@ +const fs = require('fs'); +const args = process.argv.slice(2); +fs.writeFileSync(args.shift(), JSON.stringify(process.env, null, 2), 'utf-8'); diff --git a/internal/node/test/env.spec.js b/internal/node/test/env.spec.js new file mode 100644 index 0000000000..4ec6c24e8e --- /dev/null +++ b/internal/node/test/env.spec.js @@ -0,0 +1,94 @@ +const fs = require('fs'); +const path = require('path'); +const runfiles = require(process.env['BAZEL_NODE_RUNFILES_HELPER']); +const isWindows = process.platform === 'win32'; +const runfilesExt = isWindows ? 'bat' : 'sh'; + +function normPath(p) { + let result = p.replace(/\\/g, '/'); + if (isWindows) { + // On Windows, we normalize to lowercase for so path mismatches such as 'C:/Users' and + // 'c:/users' don't break the specs. + result = result.toLowerCase(); + if (/[a-zA-Z]\:/.test(result)) { + // Handle c:/ and /c/ mismatch + result = `/${result[0]}${result.slice(2)}`; + } + } + return result; +} + +function expectPathsToMatch(a, b) { + if (Array.isArray(a) && Array.isArray(b)) { + a = a.map(p => normPath(p)); + b = b.map(p => normPath(p)); + expect(a).toEqual(b); + } else { + expect(normPath(a)).toBe(normPath(b)); + } +} + +describe('launcher.sh environment', function() { + it('should setup correct bazel environment variables when in runfiles', function() { + const runfilesRoot = normPath(process.env['RUNFILES']); + const match = runfilesRoot.match(/\/bazel-out\//); + expect(!!match).toBe(true); + const execroot = runfilesRoot.slice(0, match.index); + expectPathsToMatch(path.basename(runfilesRoot), `env_test.${runfilesExt}.runfiles`); + expectPathsToMatch(process.env['BAZEL_WORKSPACE'], 'build_bazel_rules_nodejs'); + expectPathsToMatch(process.env['BAZEL_TARGET'], '//internal/node/test:env_test'); + expectPathsToMatch(process.cwd(), `${process.env['RUNFILES']}/build_bazel_rules_nodejs`); + expectPathsToMatch(process.env['PWD'], `${process.env['RUNFILES']}/build_bazel_rules_nodejs`); + expectPathsToMatch(process.env['BAZEL_PATCH_ROOT'], process.env['RUNFILES']); + expectPathsToMatch(process.env['BAZEL_NODE_MODULES_ROOT'], 'npm/node_modules'); + const expectedGuards = [ + `${execroot}/node_modules`, + `${runfilesRoot}/npm/node_modules`, + `${runfilesRoot}/build_bazel_rules_nodejs/external/npm/node_modules`, + ] + expectPathsToMatch(process.env['BAZEL_PATCH_GUARDS'].split(','), expectedGuards); + }); + + it('should setup correct bazel environment variables when in execroot with no third party deps', + function() { + const env = require(runfiles.resolvePackageRelative('dump_build_env.json')); + // On Windows, the RUNFILES path ends in a /MANIFEST segment in this context + const runfilesRoot = normPath(isWindows ? path.dirname(env['RUNFILES']) : env['RUNFILES']); + const match = runfilesRoot.match(/\/bazel-out\//); + expect(!!match).toBe(true); + const execroot = runfilesRoot.slice(0, match.index); + expectPathsToMatch(path.basename(runfilesRoot), `dump_build_env.${runfilesExt}.runfiles`); + expectPathsToMatch(env['BAZEL_WORKSPACE'], 'build_bazel_rules_nodejs'); + expectPathsToMatch(env['BAZEL_TARGET'], '//internal/node/test:dump_build_env'); + expectPathsToMatch(env['PWD'], execroot); + expectPathsToMatch(env['BAZEL_PATCH_ROOT'], path.dirname(execroot)); + expectPathsToMatch(env['BAZEL_NODE_MODULES_ROOT'], 'build_bazel_rules_nodejs/node_modules'); + const expectedGuards = [ + `${execroot}/node_modules`, + ] + expectPathsToMatch(env['BAZEL_PATCH_GUARDS'].split(','), expectedGuards); + }); + + it('should setup correct bazel environment variables when in execroot with third party deps', + function() { + const env = require(runfiles.resolvePackageRelative('dump_build_env_alt.json')); + // On Windows, the RUNFILES path ends in a /MANIFEST segment in this context + const runfilesRoot = normPath(isWindows ? path.dirname(env['RUNFILES']) : env['RUNFILES']); + const match = runfilesRoot.match(/\/bazel-out\//); + expect(!!match).toBe(true); + const execroot = runfilesRoot.slice(0, match.index); + expectPathsToMatch( + path.basename(runfilesRoot), `dump_build_env_alt.${runfilesExt}.runfiles`); + expectPathsToMatch(env['BAZEL_WORKSPACE'], 'build_bazel_rules_nodejs'); + expectPathsToMatch(env['BAZEL_TARGET'], '//internal/node/test:dump_build_env_alt'); + expectPathsToMatch(env['PWD'], execroot); + expectPathsToMatch(env['BAZEL_PATCH_ROOT'], path.dirname(execroot)); + expectPathsToMatch(env['BAZEL_NODE_MODULES_ROOT'], 'npm/node_modules'); + const expectedGuards = [ + `${execroot}/node_modules`, + `${path.dirname(execroot)}/npm/node_modules`, + `${execroot}/external/npm/node_modules`, + ] + expectPathsToMatch(env['BAZEL_PATCH_GUARDS'].split(','), expectedGuards); + }); +}); diff --git a/internal/providers/node_runtime_deps_info.bzl b/internal/providers/node_runtime_deps_info.bzl index 67068fa48f..aa08711c48 100644 --- a/internal/providers/node_runtime_deps_info.bzl +++ b/internal/providers/node_runtime_deps_info.bzl @@ -15,6 +15,7 @@ """Custom provider that mimics the Runfiles, but doesn't incur the expense of creating the runfiles symlink tree""" load("//internal/linker:link_node_modules.bzl", "add_arg", "write_node_modules_manifest") +load("//internal/providers:npm_package_info.bzl", "NpmPackageInfo") NodeRuntimeDepsInfo = provider( doc = """Stores runtime dependencies of a nodejs_binary or nodejs_test @@ -38,6 +39,23 @@ do the same. }, ) +def _compute_node_modules_root(ctx): + """Computes the node_modules root (if any) from data & deps targets.""" + node_modules_root = "" + deps = [] + if hasattr(ctx.attr, "data"): + deps += ctx.attr.data + if hasattr(ctx.attr, "deps"): + deps += ctx.attr.deps + for d in deps: + if NpmPackageInfo in d: + possible_root = "/".join([d[NpmPackageInfo].workspace, "node_modules"]) + if not node_modules_root: + node_modules_root = possible_root + elif node_modules_root != possible_root: + fail("All npm dependencies need to come from a single workspace. Found '%s' and '%s'." % (node_modules_root, possible_root)) + return node_modules_root + def run_node(ctx, inputs, arguments, executable, **kwargs): """Helper to replace ctx.actions.run This calls node programs with a node_modules directory in place""" @@ -77,6 +95,7 @@ def run_node(ctx, inputs, arguments, executable, **kwargs): env[var] = ctx.var[var] elif var in ctx.configuration.default_shell_env.keys(): env[var] = ctx.configuration.default_shell_env[var] + env["BAZEL_NODE_MODULES_ROOT"] = _compute_node_modules_root(ctx) ctx.actions.run( inputs = inputs + extra_inputs, diff --git a/packages/node-patches/register.ts b/packages/node-patches/register.ts index 911739dc48..876408ad78 100644 --- a/packages/node-patches/register.ts +++ b/packages/node-patches/register.ts @@ -19,14 +19,15 @@ */ const patcher = require('./src'); // todo auto detect bazel env vars instead of adding a new one. -const {BAZEL_PATCH_ROOT, NP_SUBPROCESS_BIN_DIR, VERBOSE_LOGS} = process.env; +const {BAZEL_PATCH_ROOT, BAZEL_PATCH_GUARDS, NP_SUBPROCESS_BIN_DIR, VERBOSE_LOGS} = process.env; if (BAZEL_PATCH_ROOT) { + const guards = BAZEL_PATCH_GUARDS ? BAZEL_PATCH_GUARDS.split(',') : []; if (VERBOSE_LOGS) console.error(`bazel node patches enabled. root: ${ BAZEL_PATCH_ROOT} symlinks in this directory will not escape`); const fs = require('fs'); - patcher.fs(fs, BAZEL_PATCH_ROOT); + patcher.fs(fs, BAZEL_PATCH_ROOT, guards); } else if (VERBOSE_LOGS) { console.error(`bazel node patches disabled. set environment BAZEL_PATCH_ROOT`); } diff --git a/packages/node-patches/src/fs.ts b/packages/node-patches/src/fs.ts index 9807126507..dc37fbac1b 100644 --- a/packages/node-patches/src/fs.ts +++ b/packages/node-patches/src/fs.ts @@ -29,9 +29,10 @@ type Dirent = any; const _fs = require('fs'); // tslint:disable-next-line:no-any -export const patcher = (fs: any = _fs, root: string) => { +export const patcher = (fs: any = _fs, root: string, guards: string[]) => { fs = fs || _fs; root = root || ''; + guards = guards || []; if (!root) { if (process.env.VERBOSE_LOGS) { console.error('fs patcher called without root path ' + __filename); @@ -54,7 +55,7 @@ export const patcher = (fs: any = _fs, root: string) => { const origReaddir = fs.readdir.bind(fs); const origReaddirSync = fs.readdirSync.bind(fs); - const {isEscape, isOutPath} = escapeFunction(root); + const {isEscape, isOutPath} = escapeFunction(root, guards); const logged: {[k: string]: boolean} = {}; @@ -471,9 +472,10 @@ export const patcher = (fs: any = _fs, root: string) => { } }; -export const escapeFunction = (root: string) => { - // ensure root is always absolute. +export const escapeFunction = (root: string, guards: string[]) => { + // ensure root & guards are always absolute. root = path.resolve(root); + guards = guards.map(g => path.resolve(g)); function isEscape(linkTarget: string, linkPath: string) { if (!path.isAbsolute(linkPath)) { linkPath = path.resolve(linkPath); @@ -483,19 +485,32 @@ export const escapeFunction = (root: string) => { linkTarget = path.resolve(linkTarget); } + if (isGuardPath(linkPath) || isGuardPath(linkTarget)) { + // don't escape out of guard paths and don't symlink into guard paths + return true; + } + if (root) { if (isOutPath(linkTarget) && !isOutPath(linkPath)) { + // don't escape out of the root return true; } } return false; } + function isGuardPath(str) { + for (const g of guards) { + if (str === g || str.startsWith(g + path.sep)) return true; + } + return false; + } + function isOutPath(str: string) { return !root || (!str.startsWith(root + path.sep) && str !== root); } - return {isEscape, isOutPath}; + return {isEscape, isGuardPath, isOutPath}; }; function once(fn: (...args: unknown[]) => T) { diff --git a/packages/node-patches/test/fs/escape.ts b/packages/node-patches/test/fs/escape.ts index 5a50d5bda7..86d155308a 100644 --- a/packages/node-patches/test/fs/escape.ts +++ b/packages/node-patches/test/fs/escape.ts @@ -20,9 +20,25 @@ import * as path from 'path'; import {escapeFunction} from '../../src/fs'; describe('escape function', () => { - it('isOutPath is correct', () => { + it('isGuardPath & isOutPath is correct', () => { const root = '/a/b'; - const {isOutPath} = escapeFunction(root); + const guards = [ + '/a/b/g/1', + '/a/b/g/a/2', + '/a/b/g/a/3', + ]; + const {isGuardPath, isOutPath} = escapeFunction(root, guards); + + assert.ok(isGuardPath('/a/b/g/1')); + assert.ok(isGuardPath('/a/b/g/1/foo')); + assert.ok(!isGuardPath('/a/b/g/h')); + assert.ok(!isGuardPath('/a/b/g/h/i')); + assert.ok(isGuardPath('/a/b/g/a/2')); + assert.ok(isGuardPath('/a/b/g/a/2/foo')); + assert.ok(isGuardPath('/a/b/g/a/3')); + assert.ok(isGuardPath('/a/b/g/a/3/foo')); + assert.ok(!isGuardPath('/a/b/g/a/h')); + assert.ok(!isGuardPath('/a/b/g/a/h/i')); assert.ok(isOutPath('/a')); assert.ok(isOutPath('/a/c/b')); @@ -32,23 +48,121 @@ describe('escape function', () => { it('isEscape is correct', () => { const root = '/a/b'; - const {isEscape} = escapeFunction(root); + const guards = [ + '/a/b/g/1', + '/a/b/g/a/2', + '/a/b/g/a/3', + ]; + const {isEscape} = escapeFunction(root, guards); assert.ok(isEscape('/a/c/boop', '/a/b/l')); assert.ok(isEscape('/a/c/boop', '/a/b')); assert.ok(isEscape('/a', '/a/b')); assert.ok(!isEscape('/a/c/boop', '/a/c')); assert.ok(!isEscape('/a/b/f', '/a/b/l')); + + assert.ok(isEscape('/some/path', '/a/b/g/1')); + assert.ok(isEscape('/some/path', '/a/b/g/1/foo')); + assert.ok(isEscape('/some/path', '/a/b/g/h')); + assert.ok(isEscape('/some/path', '/a/b/g/h/i')); + assert.ok(isEscape('/some/path', '/a/b/g/a/2')); + assert.ok(isEscape('/some/path', '/a/b/g/a/2/foo')); + assert.ok(isEscape('/some/path', '/a/b/g/a/3')); + assert.ok(isEscape('/some/path', '/a/b/g/a/3/foo')); + assert.ok(isEscape('/some/path', '/a/b/g/a/h')); + assert.ok(isEscape('/some/path', '/a/b/g/a/h/i')); + + assert.ok(isEscape('/a/b', '/a/b/g/1')); + assert.ok(isEscape('/a/b', '/a/b/g/1/foo')); + assert.ok(!isEscape('/a/b', '/a/b/g/h')); + assert.ok(!isEscape('/a/b', '/a/b/g/h/i')); + assert.ok(isEscape('/a/b', '/a/b/g/a/2')); + assert.ok(isEscape('/a/b', '/a/b/g/a/2/foo')); + assert.ok(isEscape('/a/b', '/a/b/g/a/3')); + assert.ok(isEscape('/a/b', '/a/b/g/a/3/foo')); + assert.ok(!isEscape('/a/b', '/a/b/g/a/h')); + assert.ok(!isEscape('/a/b', '/a/b/g/a/h/i')); + + assert.ok(isEscape('/a/b/c', '/a/b/g/1')); + assert.ok(isEscape('/a/b/c', '/a/b/g/1/foo')); + assert.ok(!isEscape('/a/b/c', '/a/b/g/h')); + assert.ok(!isEscape('/a/b/c', '/a/b/g/h/i')); + assert.ok(isEscape('/a/b/c', '/a/b/g/a/2')); + assert.ok(isEscape('/a/b/c', '/a/b/g/a/2/foo')); + assert.ok(isEscape('/a/b/c', '/a/b/g/a/3')); + assert.ok(isEscape('/a/b/c', '/a/b/g/a/3/foo')); + assert.ok(!isEscape('/a/b/c', '/a/b/g/a/h')); + assert.ok(!isEscape('/a/b/c', '/a/b/g/a/h/i')); + + assert.ok(isEscape('/a/b/g/1', '/some/path')); + assert.ok(isEscape('/a/b/g/1/foo', '/some/path')); + assert.ok(!isEscape('/a/b/g/h', '/some/path')); + assert.ok(!isEscape('/a/b/g/h/i', '/some/path')); + assert.ok(isEscape('/a/b/g/a/2', '/some/path')); + assert.ok(isEscape('/a/b/g/a/2/foo', '/some/path')); + assert.ok(isEscape('/a/b/g/a/3', '/some/path')); + assert.ok(isEscape('/a/b/g/a/3/foo', '/some/path')); + assert.ok(!isEscape('/a/b/g/a/h', '/some/path')); + assert.ok(!isEscape('/a/b/g/a/h/i', '/some/path')); }); it('isEscape handles relative paths', () => { const root = './a/b'; - const {isEscape} = escapeFunction(root); + const guards = [ + './a/b/g/1', + './a/b/g/a/2', + './a/b/g/a/3', + ]; + const {isEscape} = escapeFunction(root, guards); assert.ok(isEscape('./a/c/boop', './a/b/l')); assert.ok(isEscape('./a/c/boop', './a/b')); assert.ok(isEscape('./a', './a/b')); assert.ok(!isEscape('./a/c/boop', './a/c')); assert.ok(!isEscape('./a/b/f', './a/b/l')); + + assert.ok(isEscape('./some/path', './a/b/g/1')); + assert.ok(isEscape('./some/path', './a/b/g/1/foo')); + assert.ok(isEscape('./some/path', './a/b/g/h')); + assert.ok(isEscape('./some/path', './a/b/g/h/i')); + assert.ok(isEscape('./some/path', './a/b/g/a/2')); + assert.ok(isEscape('./some/path', './a/b/g/a/2/foo')); + assert.ok(isEscape('./some/path', './a/b/g/a/3')); + assert.ok(isEscape('./some/path', './a/b/g/a/3/foo')); + assert.ok(isEscape('./some/path', './a/b/g/a/h')); + assert.ok(isEscape('./some/path', './a/b/g/a/h/i')); + + assert.ok(isEscape('./a/b', './a/b/g/1')); + assert.ok(isEscape('./a/b', './a/b/g/1/foo')); + assert.ok(!isEscape('./a/b', './a/b/g/h')); + assert.ok(!isEscape('./a/b', './a/b/g/h/i')); + assert.ok(isEscape('./a/b', './a/b/g/a/2')); + assert.ok(isEscape('./a/b', './a/b/g/a/2/foo')); + assert.ok(isEscape('./a/b', './a/b/g/a/3')); + assert.ok(isEscape('./a/b', './a/b/g/a/3/foo')); + assert.ok(!isEscape('./a/b', './a/b/g/a/h')); + assert.ok(!isEscape('./a/b', './a/b/g/a/h/i')); + + assert.ok(isEscape('./a/b/c', './a/b/g/1')); + assert.ok(isEscape('./a/b/c', './a/b/g/1/foo')); + assert.ok(!isEscape('./a/b/c', './a/b/g/h')); + assert.ok(!isEscape('./a/b/c', './a/b/g/h/i')); + assert.ok(isEscape('./a/b/c', './a/b/g/a/2')); + assert.ok(isEscape('./a/b/c', './a/b/g/a/2/foo')); + assert.ok(isEscape('./a/b/c', './a/b/g/a/3')); + assert.ok(isEscape('./a/b/c', './a/b/g/a/3/foo')); + assert.ok(!isEscape('./a/b/c', './a/b/g/a/h')); + assert.ok(!isEscape('./a/b/c', './a/b/g/a/h/i')); + + assert.ok(isEscape('./a/b/g/1', './some/path')); + assert.ok(isEscape('./a/b/g/1/foo', './some/path')); + assert.ok(!isEscape('./a/b/g/h', './some/path')); + assert.ok(!isEscape('./a/b/g/h/i', './some/path')); + assert.ok(isEscape('./a/b/g/a/2', './some/path')); + assert.ok(isEscape('./a/b/g/a/2/foo', './some/path')); + assert.ok(isEscape('./a/b/g/a/3', './some/path')); + assert.ok(isEscape('./a/b/g/a/3/foo', './some/path')); + assert.ok(!isEscape('./a/b/g/a/h', './some/path')); + assert.ok(!isEscape('./a/b/g/a/h/i', './some/path')); }); }); diff --git a/packages/node-patches/test/fs/lstat.ts b/packages/node-patches/test/fs/lstat.ts index 8ac033acd7..5cecd219a2 100644 --- a/packages/node-patches/test/fs/lstat.ts +++ b/packages/node-patches/test/fs/lstat.ts @@ -33,12 +33,12 @@ describe('testing lstat', () => { }, async fixturesDir => { fixturesDir = fs.realpathSync(fixturesDir); - // create symlink from a to b + // create symlink from a/link to b/file fs.symlinkSync(path.join(fixturesDir, 'b', 'file'), path.join(fixturesDir, 'a', 'link')); const patchedFs = Object.assign({}, fs); patchedFs.promises = Object.assign({}, fs.promises); - patcher(patchedFs, path.join(fixturesDir)); + patcher(patchedFs, path.join(fixturesDir), []); const linkPath = path.join(fixturesDir, 'a', 'link'); assert.ok( @@ -55,6 +55,37 @@ describe('testing lstat', () => { }); }); + it('can lstat symlink in guard is file', async () => { + await withFixtures( + { + a: {g: {}}, + b: {file: 'contents'}, + }, + async fixturesDir => { + fixturesDir = fs.realpathSync(fixturesDir); + // create symlink from a/g/link to b/file + fs.symlinkSync( + path.join(fixturesDir, 'b', 'file'), path.join(fixturesDir, 'a', 'g', 'link')); + + const patchedFs = Object.assign({}, fs); + patchedFs.promises = Object.assign({}, fs.promises); + patcher(patchedFs, path.join(fixturesDir), [path.join(fixturesDir, 'a', 'g')]); + + const linkPath = path.join(fixturesDir, 'a', 'g', 'link'); + assert.ok( + patchedFs.lstatSync(linkPath).isFile(), + 'lstatSync should find file if link is in guard'); + + assert.ok( + (await util.promisify(patchedFs.lstat)(linkPath)).isFile(), + 'lstat should find file if link is in guard'); + + assert.ok( + (await patchedFs.promises.lstat(linkPath)).isFile(), + 'promises.lstat should find file if link is in guard'); + }); + }); + it('lstat of symlink out of root is file.', async () => { await withFixtures( { @@ -63,26 +94,26 @@ describe('testing lstat', () => { }, async fixturesDir => { fixturesDir = fs.realpathSync(fixturesDir); - // create symlink from a to b + // create symlink from a/link to b/file fs.symlinkSync(path.join(fixturesDir, 'b', 'file'), path.join(fixturesDir, 'a', 'link')); const patchedFs = Object.assign({}, fs); patchedFs.promises = Object.assign({}, fs.promises); - patcher(patchedFs, path.join(fixturesDir, 'a')); + patcher(patchedFs, path.join(fixturesDir, 'a'), []); const linkPath = path.join(fixturesDir, 'a', 'link'); assert.ok( patchedFs.lstatSync(linkPath).isFile(), - 'lstatSync should find file it file linked is out of root'); + 'lstatSync should find file it file link is out of root'); assert.ok( (await util.promisify(patchedFs.lstat)(linkPath)).isFile(), - 'lstat should find file it file linked is out of root'); + 'lstat should find file it file link is out of root'); assert.ok( (await patchedFs.promises.lstat(linkPath)).isFile(), - 'promises.lstat should find file it file linked is out of root'); + 'promises.lstat should find file it file link is out of root'); let brokenLinkPath = path.join(fixturesDir, 'a', 'broken-link'); fs.symlinkSync(path.join(fixturesDir, 'doesnt-exist'), brokenLinkPath); @@ -109,12 +140,12 @@ describe('testing lstat', () => { }, async fixturesDir => { fixturesDir = fs.realpathSync(fixturesDir); - // create symlink from a to b + // create symlink from a/link to b/file fs.symlinkSync(path.join(fixturesDir, 'b', 'file'), path.join(fixturesDir, 'b', 'link')); const patchedFs = Object.assign({}, fs); patchedFs.promises = Object.assign({}, fs.promises); - patcher(patchedFs, path.join(fixturesDir, 'a')); + patcher(patchedFs, path.join(fixturesDir, 'a'), []); const linkPath = path.join(fixturesDir, 'b', 'link'); diff --git a/packages/node-patches/test/fs/opendir.ts b/packages/node-patches/test/fs/opendir.ts index 00b86996b4..96e4e1707b 100644 --- a/packages/node-patches/test/fs/opendir.ts +++ b/packages/node-patches/test/fs/opendir.ts @@ -39,7 +39,7 @@ describe('testing opendir', () => { const patchedFs = Object.assign({}, fs); patchedFs.promises = Object.assign({}, fs.promises); - patcher(patchedFs, fixturesDir); + patcher(patchedFs, fixturesDir, []); (patchedFs as any).DEBUG = true; let dir; @@ -74,7 +74,7 @@ describe('testing opendir', () => { const patchedFs = Object.assign({}, fs); patchedFs.promises = Object.assign({}, fs.promises); - patcher(patchedFs, path.join(fixturesDir, 'a')); + patcher(patchedFs, path.join(fixturesDir, 'a'), []); (patchedFs as any).DEBUG = true; let dir; @@ -109,7 +109,7 @@ describe('testing opendir', () => { const patchedFs = Object.assign({}, fs); patchedFs.promises = Object.assign({}, fs.promises); - patcher(patchedFs, path.join(fixturesDir)); + patcher(patchedFs, path.join(fixturesDir), []); (patchedFs as any).DEBUG = true; const dir = await util.promisify(patchedFs.opendir)(path.join(fixturesDir, 'a')); @@ -140,7 +140,7 @@ describe('testing opendir', () => { const patchedFs = Object.assign({}, fs); patchedFs.promises = Object.assign({}, fs.promises); - patcher(patchedFs, path.join(fixturesDir, 'a')); + patcher(patchedFs, path.join(fixturesDir, 'a'), []); (patchedFs as any).DEBUG = true; const dir = await util.promisify(patchedFs.opendir)(path.join(fixturesDir, 'a')); diff --git a/packages/node-patches/test/fs/readdir.ts b/packages/node-patches/test/fs/readdir.ts index 26cc1cdbfb..14a95bef36 100644 --- a/packages/node-patches/test/fs/readdir.ts +++ b/packages/node-patches/test/fs/readdir.ts @@ -38,7 +38,7 @@ describe('testing readdir', () => { const patchedFs = Object.assign({}, fs); patchedFs.promises = Object.assign({}, fs.promises); - patcher(patchedFs, fixturesDir); + patcher(patchedFs, fixturesDir, []); let dirents = patchedFs.readdirSync(path.join(fixturesDir, 'a'), { withFileTypes: true, @@ -77,7 +77,7 @@ describe('testing readdir', () => { const patchedFs = Object.assign({}, fs); patchedFs.promises = Object.assign({}, fs.promises); - patcher(patchedFs, path.join(fixturesDir, 'a')); + patcher(patchedFs, path.join(fixturesDir, 'a'), []); let dirents = patchedFs.readdirSync(path.join(fixturesDir, 'a'), { withFileTypes: true, diff --git a/packages/node-patches/test/fs/readlink.ts b/packages/node-patches/test/fs/readlink.ts index 34259a4694..b26ce50ce8 100644 --- a/packages/node-patches/test/fs/readlink.ts +++ b/packages/node-patches/test/fs/readlink.ts @@ -38,7 +38,7 @@ describe('testing readlink', () => { const patchedFs = Object.assign({}, fs); patchedFs.promises = Object.assign({}, fs.promises); - patcher(patchedFs, path.join(fixturesDir)); + patcher(patchedFs, path.join(fixturesDir), []); const linkPath = path.join(fixturesDir, 'a', 'link'); assert.deepStrictEqual( @@ -70,7 +70,7 @@ describe('testing readlink', () => { const patchedFs = Object.assign({}, fs); patchedFs.promises = Object.assign({}, fs.promises); - patcher(patchedFs, path.join(fixturesDir, 'a')); + patcher(patchedFs, path.join(fixturesDir, 'a'), []); const linkPath = path.join(fs.realpathSync(fixturesDir), 'a', 'link'); assert.throws(() => { diff --git a/packages/node-patches/test/fs/realpath.ts b/packages/node-patches/test/fs/realpath.ts index 664471b9e1..d8496c96ed 100644 --- a/packages/node-patches/test/fs/realpath.ts +++ b/packages/node-patches/test/fs/realpath.ts @@ -40,7 +40,7 @@ describe('testing realpath', () => { const patchedFs = Object.assign({}, fs); patchedFs.promises = Object.assign({}, fs.promises); - patcher(patchedFs, path.join(fixturesDir)); + patcher(patchedFs, path.join(fixturesDir), []); const linkPath = path.join(fs.realpathSync(fixturesDir), 'a', 'link'); assert.deepStrictEqual( @@ -83,7 +83,7 @@ describe('testing realpath', () => { const patchedFs = Object.assign({}, fs); patchedFs.promises = Object.assign({}, fs.promises); - patcher(patchedFs, path.join(fixturesDir, 'a')); + patcher(patchedFs, path.join(fixturesDir, 'a'), []); const linkPath = path.join(fs.realpathSync(fixturesDir), 'a', 'link'); assert.deepStrictEqual(