Block Unsafe Deserialization and Shell Injection in Python

Flags pickle, marshal, and unsafe YAML loaders on untrusted data, shell=True commands built from input, option injection, and eval or exec on user input.

What it flags

Flaggedexports/worker.py
import pickle
import subprocess

import yaml


def run_export(raw_job: bytes) -> str:
    # Untrusted queue data reaches pickle, an unsafe YAML loader, and a shell
    job = pickle.loads(raw_job)
    config = yaml.load(job["config"], Loader=yaml.Loader)
    out_path = f"/srv/exports/{job['id']}.pdf"
    subprocess.run(
        f"render-report --title '{config['title']}' --out {out_path}",
        shell=True,
        check=True,
    )
    return out_path
Passesexports/worker.py
import json
import subprocess

import yaml


def run_export(raw_job: bytes) -> str:
    job = json.loads(raw_job)
    config = yaml.safe_load(job["config"])
    job_id = int(job["id"])
    out_path = f"/srv/exports/{job_id}.pdf"
    subprocess.run(
        ["render-report", f"--title={config['title']}", f"--out={out_path}"],
        check=True,
        timeout=120,
    )
    return out_path

Why it matters

Unpickling attacker-controlled bytes, loading YAML with an unsafe loader, or calling eval on input runs code the attacker chose, and shell=True hands every quote and semicolon in an interpolated value to the shell. The dangerous input often arrives indirectly, through a queue message, a cache entry, an uploaded model file, or a filename, so the trust boundary sits several calls away from the vulnerable line. Each of these leads to remote code execution with the permissions of the service.

Agent instructions

cubic checks every pull request against these instructions. Paste them into AGENTS.md or Cursor rules and your AI coding tool follows them while it writes code.

AGENTS.md

Add this section to AGENTS.md at the repository root. Codex, Cursor, and other AI coding tools read it.

## Block Unsafe Deserialization and Shell Injection in Python

Applies to `**/*.py`.

Review Python code that parses serialized data or runs commands for paths where untrusted input can execute code.

Flag changes that:
- Call `pickle.load`, `pickle.loads`, `marshal.loads`, `shelve.open`, `dill.loads`, `joblib.load`, or `pandas.read_pickle` on data from requests, uploads, message queues, shared caches, or storage that other parties can write to.
- Call `torch.load` with `weights_only=False`, the default before PyTorch 2.6, or `numpy.load(..., allow_pickle=True)` on files the service did not produce.
- Parse YAML from outside the codebase with `yaml.unsafe_load`, or with `yaml.load` and `Loader=yaml.Loader` or `yaml.UnsafeLoader`; use `yaml.safe_load`.
- Run `subprocess` with `shell=True`, `os.system`, or `os.popen` on a command string built from input with f-strings, `%`, `.format()`, or concatenation.
- Pass user input as a positional argument to tools such as `git`, `curl`, `tar`, or `ssh` without validating it or adding a `--` separator, which allows option injection without a shell.
- Call `eval`, `exec`, or `compile` on input, including variants that strip `__builtins__`; use `json.loads` or `ast.literal_eval` for data.

Do not flag:
- Pickle or joblib files the same service writes and reads from storage no other party can write to, such as a local cache directory.
- `subprocess` calls that pass a list of arguments built from constants or validated values, or `shell=True` with a constant string.
- Test fixtures, build scripts, and developer tooling that only read files from the repository.

Checks files matching **/*.py.

More Python agents

See all Python code review standards

Run this agent on every pull request

cubic reviews every pull request against the agents you turn on and comments on the lines that break them.

Catch this in your code