| Evaluate an expression string | eval(expr) / exec(s), even from a "trusted" template | a small ast-walk allowlist evaluator (numeric nodes only), or sympy.sympify(expr, rational=True) for math. ast.literal_eval parses literals only — it will NOT evaluate a+b, so it is not a drop-in for arithmetic. |
| Run a subprocess | subprocess.run(cmd, shell=True), os.system(s), os.popen(s), a bare "git" (partial path) | pass an argv list with shell=False (the default) and a full or PATH-resolved executable; wrap any interpolated value in shlex.quote. If a shell script really must be generated, pass operator values in through the environment ($VAR) and read them there — never f-string them into the script text. |
| Build a SQL statement | f-string or + with a variable | a parameterized query: cur.execute("... WHERE id = ?", (id,)). Never string-format SQL, not even for an integer. |
| Deserialize | pickle.load, yaml.load(f) with the full loader, torch.load(path) on a file you did not write | json.load, yaml.safe_load, torch.load(path, weights_only=True). Never unpickle data from outside your own process. |
| Extract an archive | tarfile.extractall() / zipfile.extractall() on a downloaded file | resolve each member path against the target directory and reject anything that escapes it (path traversal / zip-slip) before writing: os.path.realpath(os.path.join(dest, name)).startswith(os.path.realpath(dest) + os.sep). Reject absolute member names and symlink members too. |
| Fetch a URL | urllib.request.urlopen(url) on a caller-influenced url, or any fetch with no timeout | check the parsed scheme is https, check the host against an allowlist, and always pass timeout= (requests.get(url, timeout=10)). A missing timeout is a hang, not just a style nit. |
| Hash for a security decision | hashlib.md5(x) / hashlib.sha1(x) as an integrity or auth check | hashlib.sha256. For non-security content addressing or dedup, md5 is fine but say so in code: hashlib.md5(b, usedforsecurity=False). |
| Temp file | a hardcoded /tmp/thing.json | tempfile.mkstemp() or tempfile.TemporaryDirectory() — unpredictable name, correct permissions, cleaned up. |
| Bind a server | 0.0.0.0 by default | bind 127.0.0.1 unless a remote bind is the deliberate, stated intent. |
| File permissions | os.chmod(p, 0o777) | least privilege: 0o600 for files, 0o700 for directories, 0o755 only for a script that is meant to be executable. |
| Randomness for a token, key, nonce, or password | random.random(), random.choice, random.randint | secrets.token_hex / secrets.choice / os.urandom. (random is correct for sampling, shuffling, and seeded data generation — the distinction is whether an attacker guessing the value matters.) |
| Log or print an object that carries a secret | print(json.dumps(payload)) after "sanitizing" one field | build a separate display object into which the real secret never lands — see below. |