Skip to content

Commit

Permalink
Throw an error when esbuild loads a file outside the bazel sandbox.
Browse files Browse the repository at this point in the history
This implementation uses an OnLoad plugin to catch when a file is loaded that is
not in an allowlist of files. The allowlist is all the files within the
BAZEL_BINDIR and all of the symlink targets of those files.

This may not prevent all sandbox escaping modes. The esbuild Go code may still
access unsandboxed files in the course of loading files that are in the sanbox.

Addresses aspect-build#58 and requires
aspect-build/rules_js#793 to work properly.
  • Loading branch information
gonzojive committed Jan 15, 2023
1 parent 64daf7b commit 30b2785
Showing 1 changed file with 114 additions and 2 deletions.
116 changes: 114 additions & 2 deletions esbuild/private/launcher.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
const { readFileSync, writeFileSync } = require('fs')
const _fs = require('fs');
// Use the _unpatched extension of fs from
// https://github.com/aspect-build/rules_js/pull/793.
const { readFileSync, writeFileSync, readdirSync, realpathSync } = _fs._unpatched || _fs;
const { pathToFileURL } = require('url')
const { join } = require('path')
const { join, resolve } = require('path')
const esbuild = require('esbuild')

function getFlag(flag, required = true) {
Expand Down Expand Up @@ -97,6 +100,25 @@ async function processConfigFile(configFilePath, existingArgs = {}) {
}, {})
}

const bazelSandboxPlugin = {
name: 'Bazel Sandbox Guard',
setup(build) {
// Generate an allowlist with all the files and the targets of symlinks from
// the bin directory for this execution.
//
// Note that process.cwd() appears to already be BAZEL_BINDIR.
const sandbox = new SandboxContents(process.cwd());
// See https://esbuild.github.io/plugins/#on-load-arguments for docs about
// onLoad.
build.onLoad({ filter: /.*/ }, args => {
sandbox.checkFileIsInSandbox(args.path);
});
}
}


// process.exit(1);

if (!process.env.ESBUILD_BINARY_PATH) {
console.error('Expected environment variable ESBUILD_BINARY_PATH to be set')
process.exit(1)
Expand All @@ -118,6 +140,19 @@ async function runOneBuild(args, userArgsFilePath, configFilePath) {
}
}

// If running under rules_js, add a plugin that attempts to restrict file
// system access within the sandbox.
if (process.env.BAZEL_BINDIR) {
if (args.hasOwnProperty('plugins')) {
args.plugins.push(bazelSandboxPlugin)
} else {
args.plugins = [bazelSandboxPlugin]
}

// Never preserve symlinks as this breaks the pnpm node_modules layout.
args.preserveSymlinks = false
}

try {
const result = await esbuild.build(args)
if (result.metafile) {
Expand All @@ -130,6 +165,83 @@ async function runOneBuild(args, userArgsFilePath, configFilePath) {
}
}

/**
* An index of files within the sandbox and some methods for checking that a
* given path is within the sandbox.
*/
class SandboxContents {
/**
* @param {string} sandboxRoot Path to root of sandbox.
*/
constructor(sandboxRoot) {
this._files = listAllFiles(sandboxRoot);
this._allowedPaths = new Set();
this._files.forEach(f => {
this._allowedPaths.add(f.realPathResolved);
this._allowedPaths.add(f.pathResolved);
});
}

/**
* Returns true if the given path is in the sandbox.
*
* @param {string} absPath The absolute path of some file.
* @returns {boolean} true if the file is in the sandbox.
*/
inSandbox(absPath) {
return this._allowedPaths.has(absPath);
}

/**
* @returns {string} debug summary of the sandbox contents.
*/
sandboxSummary(indent) {
indent = indent || '';
return this._files.map((entry) => {
if (entry.isSymbolicLink) {
return `${indent}${entry.pathResolved} ->\n${indent} ${entry.realPathResolved}`;
}
return indent + entry.realPathResolved;
}).join('\n');
}

/**
* @param {string} somePath path to some file.
* @throws {Error} if the path is not in the sandbox.
*/
checkFileIsInSandbox(somePath) {
const absPath = resolve(realpathSync(somePath));
if (this.inSandbox(absPath)) {
return;
}

throw new Error(
`loaded file is not allowed because the file is not within the bazel ` +
`sandbox. Check the deps of the esbuild rule. \n` +
`${absPath} is not in list of ${this._files.length} sandbox entries:\n` +
`${this.sandboxSummary()}`);
}
}

function listAllFiles(folder) {
const out = [];
readdirSync(folder, {withFileTypes: true}).forEach(file => {
const fileName = join(folder, file.name);
if (file.isDirectory()) {
out.push(...listAllFiles(fileName));
} else {
const realPath = realpathSync(fileName);
out.push({
path: fileName,
pathResolved: resolve(fileName),
isSymbolicLink: file.isSymbolicLink(),
realPathResolved: resolve(realPath),
});
}
});
return out;
}

runOneBuild(
getEsbuildArgs(getFlag('--esbuild_args')),
getFlag('--user_args', false),
Expand Down

0 comments on commit 30b2785

Please sign in to comment.