Files
shockbot/prep/installPythonDependencies.ts
T
David Blass bc28c658f2 harden sandbox escape vectors for bash disabled/restricted modes (#257)
* harden sandbox escape vectors for bash disabled/restricted modes

block git config injection (-c flag as subcommand), dangerous subcommands
(config, submodule, rebase, bisect), code-executing arg flags (--exec,
--extcmd), .gitattributes/.gitmodules writes, and package lifecycle scripts.
add retry logic to test runner for transient failures. add security unit
tests and adhoc attack tests.

Co-authored-by: Cursor <cursoragent@cursor.com>

* only filter subcommands in nobash, remove nobash from ui

* use regex matching

* iterate on tests

* simplify githooks

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-11 02:02:41 +00:00

192 lines
5.4 KiB
TypeScript

import { existsSync } from "node:fs";
import { join } from "node:path";
import { log } from "../utils/cli.ts";
import { spawn } from "../utils/subprocess.ts";
import type {
PrepDefinition,
PrepOptions,
PythonPackageManager,
PythonPrepResult,
} from "./types.ts";
interface PythonConfig {
file: string;
tool: PythonPackageManager;
installCmd: string[];
}
// python dependency file patterns in priority order
const PYTHON_CONFIGS: PythonConfig[] = [
{
file: "requirements.txt",
tool: "pip",
installCmd: ["pip", "install", "-r", "requirements.txt"],
},
{
file: "pyproject.toml",
tool: "pip",
installCmd: ["pip", "install", "."],
},
{
file: "Pipfile",
tool: "pipenv",
installCmd: ["pipenv", "install"],
},
{
file: "Pipfile.lock",
tool: "pipenv",
installCmd: ["pipenv", "sync"],
},
{
file: "poetry.lock",
tool: "poetry",
installCmd: ["poetry", "install", "--no-interaction"],
},
{
file: "setup.py",
tool: "pip",
installCmd: ["pip", "install", "-e", "."],
},
];
// tool install commands (via pip)
const TOOL_INSTALL_COMMANDS: Record<string, string[]> = {
pipenv: ["pip", "install", "pipenv"],
poetry: ["pip", "install", "poetry"],
};
async function isCommandAvailable(command: string): Promise<boolean> {
const result = await spawn({
cmd: "which",
args: [command],
env: { PATH: process.env.PATH || "" },
});
return result.exitCode === 0;
}
async function installTool(name: string): Promise<string | null> {
const installCmd = TOOL_INSTALL_COMMANDS[name];
if (!installCmd) {
// tool doesn't need installation (e.g., pip)
return null;
}
log.info(`» installing ${name}...`);
const [cmd, ...args] = installCmd;
const result = await spawn({
cmd,
args,
env: { PATH: process.env.PATH || "", HOME: process.env.HOME || "" },
onStderr: (chunk) => process.stderr.write(chunk),
});
if (result.exitCode !== 0) {
return result.stderr || `failed to install ${name}`;
}
log.info(`» installed ${name}`);
return null;
}
export const installPythonDependencies: PrepDefinition = {
name: "installPythonDependencies",
shouldRun: async () => {
// check if python is available
const hasPython = (await isCommandAvailable("python3")) || (await isCommandAvailable("python"));
if (!hasPython) {
return false;
}
// check if any python config file exists
const cwd = process.cwd();
return PYTHON_CONFIGS.some((config) => existsSync(join(cwd, config.file)));
},
run: async (options: PrepOptions): Promise<PythonPrepResult> => {
const cwd = process.cwd();
// find the first matching config
const config = PYTHON_CONFIGS.find((c) => existsSync(join(cwd, c.file)));
if (!config) {
return {
language: "python",
packageManager: "pip",
configFile: "unknown",
dependenciesInstalled: false,
issues: ["no python config file found"],
};
}
log.info(`» detected python config: ${config.file} (using ${config.tool})`);
// SECURITY: when bash is disabled, skip ALL python dependency installation.
// every python install path can potentially execute arbitrary code:
// - setup.py / pyproject.toml: directly execute build backends
// - requirements.txt: can contain "-e ." or local path references that
// trigger setup.py execution
// - Pipfile/poetry.lock: can contain path dependencies pointing to local
// directories with malicious setup.py
// - source distributions from PyPI also execute setup.py
// there is no equivalent of npm's --ignore-scripts for pip.
if (options.ignoreScripts) {
log.info(
`» skipping python install (bash disabled, python packages can execute arbitrary code)`
);
return {
language: "python",
packageManager: config.tool,
configFile: config.file,
dependenciesInstalled: false,
issues: [
`skipped: python dependency installation can execute arbitrary code (setup.py, build backends, local path references), which is blocked when bash is disabled`,
],
};
}
// check if the tool is available, install if needed
const isAvailable = await isCommandAvailable(config.tool);
if (!isAvailable) {
log.info(`» ${config.tool} not found, attempting to install...`);
const installError = await installTool(config.tool);
if (installError) {
return {
language: "python",
packageManager: config.tool,
configFile: config.file,
dependenciesInstalled: false,
issues: [installError],
};
}
}
// run the install command
const [cmd, ...args] = config.installCmd;
log.info(`» running: ${cmd} ${args.join(" ")}`);
const result = await spawn({
cmd,
args,
env: { PATH: process.env.PATH || "", HOME: process.env.HOME || "" },
onStderr: (chunk) => process.stderr.write(chunk),
});
if (result.exitCode !== 0) {
return {
language: "python",
packageManager: config.tool,
configFile: config.file,
dependenciesInstalled: false,
issues: [result.stderr || `${cmd} exited with code ${result.exitCode}`],
};
}
return {
language: "python",
packageManager: config.tool,
configFile: config.file,
dependenciesInstalled: true,
issues: [],
};
},
};