Go cheatsheet covering the go toolchain, syntax essentials, goroutines and channels, table-driven …
Python Security Cheatsheet Python Security Cheatsheet

Summary
Most of the security bugs I have found in automation scripts are not clever. They are a string passed to a shell, a config file loaded with the wrong function, or a token that ended up in CloudWatch. This sheet is the short list of rules I check before a Python script gets anywhere near a server, with the safe form next to the one that bites. The Python Logging Cheatsheet covers the other half of the secret-leaking problem.
shell=True with an interpolated string, yaml.load on a file you did not write, and a secret in a log line. Everything else on this sheet is a distant fourth.subprocess Without a Shell
import subprocess, shlex
# NO: the string is parsed by /bin/sh
subprocess.run(f"kubectl delete pod {name}", shell=True)
# name = "web; rm -rf /tmp/x" -> two commands run
# YES: a list, no shell, arguments passed straight to execve
subprocess.run(["kubectl", "delete", "pod", name], check=True, timeout=30)
Both of these were run with the same input. The first one is the bug:
Expand your knowledge with Go Security Cheatsheet
$ python3 delete_pod.py 'web-1; echo OWNED'deleting pod web-1OWNED# /bin/sh split the argument at the semicolon and ran the second half.# The exit code is 0, so nothing upstream will ever notice.$ echo $? → 0$ python3 delete_pod_safe.py 'web-1; echo OWNED'deleting pod web-1; echo OWNED# One argument, handed to execve untouched. No shell, nothing to split.@@ -3,4 +3,4 @@ import subprocess name = sys.argv[1]-subprocess.run(f"kubectl delete pod {name}", shell=True)+subprocess.run(["kubectl", "delete", "pod", name], check=True, timeout=30)| Want | Do |
|---|---|
| Fail loudly on non-zero exit | check=True |
| Capture output as text | capture_output=True, text=True |
| Never hang in CI | timeout=30 |
| A literal pipeline | two Popens, or shell=True with no interpolation |
| Quote a value you must put in a shell string | shlex.quote(value) |
r = subprocess.run(["aws", "sts", "get-caller-identity"],
capture_output=True, text=True, check=True, timeout=15)
identity = json.loads(r.stdout)
shlex.quote() is a patch, not a fix. It is correct for POSIX sh and wrong for Windows cmd. If you find yourself reaching for it, check whether the list form would do instead — it almost always does.Deserialisation: safe_load, and Never pickle
Docs: PyYAML loading safely
import yaml
cfg = yaml.safe_load(open("values.yaml")) # scalars, lists, dicts only
cfg = yaml.load(open("values.yaml")) # can construct any Python object
A file carrying a !!python/object/apply tag runs that callable the moment the
full loader touches it. safe_load has no such tags:
$ cat values.yamlcfg: !!python/object/apply:os.system ["echo OWNED-BY-CONFIG"]$ python3 -c 'import yaml; print(yaml.safe_load(open("values.yaml")))'ConstructorError: could not determine a constructor for the tag 'tag:yaml.org,2002:python/object/apply:os.system'$ python3 -c 'import yaml; print(yaml.load(open("values.yaml"), Loader=yaml.UnsafeLoader))'OWNED-BY-CONFIG{'cfg': 0}# The echo ran before the dict was even returned. The 0 is its exit status,# stored as the value of cfg — which is how little the loader cares.import pickle, json
pickle.load(open(path, "rb")) # arbitrary code execution, by design
json.load(open(path)) # data only
pickle is not a file format, it is a small program the unpickler executes. Never load a pickle you did not write yourself, and never one that crossed a network or a shared bucket. Use JSON for data interchange.Others in the same family: eval(), exec(), os.system(), marshal, and xml against untrusted input (use defusedxml).
Deepen your understanding in Bash Basics Cheatsheet
Secrets and Randomness
import secrets
secrets.token_urlsafe(32) # URL-safe token, ~43 chars
secrets.token_hex(16) # 32 hex chars
secrets.choice(alphabet) # unguessable pick
secrets.compare_digest(a, b) # constant-time comparison
import random
random.choice(alphabet) # predictable: never for anything secret
random is a Mersenne Twister. Observe enough of its output and you can reconstruct the state and predict the rest — fine for jitter and sampling, disqualifying for tokens.
Explore this further in Go Security Cheatsheet
# Comparing a token with == leaks its length and prefix through timing.
if secrets.compare_digest(provided_token, expected_token):
...
Passwords
Docs: hashlib.scrypt
# NO: fast hashes are built to be fast, which is the wrong property here
hashlib.sha256(password.encode()).hexdigest()
hashlib.md5(password.encode()).hexdigest()
# YES: a slow, salted KDF
import hashlib, os
salt = os.urandom(16)
key = hashlib.scrypt(password.encode(), salt=salt, n=2**14, r=8, p=1, dklen=32)
# Better still, if you can take the dependency:
# argon2-cffi -> argon2.PasswordHasher().hash(password)
# bcrypt -> bcrypt.hashpw(pw, bcrypt.gensalt())
SHA-256 and MD5 are correct for checksums and content addressing, and wrong for passwords for exactly the reason they are good at checksums.
Discover related concepts in Go (Golang) Cheatsheet
SQL: Parameters, Not Formatting
# NO: all three build SQL out of a value
cur.execute(f"SELECT * FROM pods WHERE name = '{name}'")
cur.execute("SELECT * FROM pods WHERE name = " + name)
cur.execute("SELECT * FROM pods WHERE name = %s" % name)
# YES: the driver sends the value separately from the statement
cur.execute("SELECT * FROM pods WHERE name = %s", (name,)) # psycopg
cur.execute("SELECT * FROM pods WHERE name = ?", (name,)) # sqlite3
cur.execute("SELECT * FROM pods WHERE name = %(n)s", {"n": name}) # named
TLS and HTTP
import requests
requests.get(url, timeout=10) # verify=True is the default
requests.get(url, timeout=10, verify=False) # NO: accepts any certificate
requests.get(url, timeout=10, verify="/etc/ssl/corp-ca.pem") # private CA
verify=False turns an encrypted channel into one anybody on the path can read and rewrite — and it prints a warning that people then suppress. A self-signed or internal certificate is a reason to pass the CA bundle with verify=, not to stop checking.- Always pass
timeout=. Without it a request can hang forever and hold a worker, which is a denial of service you did to yourself. allow_redirects=Falsewhen the redirect target matters, so a server cannot steer you somewhere else.- A
Sessionkeeps headers across requests — including anAuthorizationheader you did not mean to send to a redirected host.
Secrets at Rest and in Transit
import os
token = os.environ["API_TOKEN"] # KeyError if missing: fail loudly
token = os.environ.get("API_TOKEN") # None, then a confusing error later
# Pull from the platform rather than baking it in
import boto3
sm = boto3.client("secretsmanager")
secret = sm.get_secret_value(SecretId="prod/api")["SecretString"]
ssm = boto3.client("ssm")
val = ssm.get_parameter(Name="/prod/token", WithDecryption=True)["Parameter"]["Value"]
Rules that survive contact with a real repository:
Enrich your learning with Go Security Cheatsheet
- No secret in source, in a default argument, or in a committed
.env. - No secret in a log line, an exception message or a traceback — see the logging sheet.
- No secret in a URL; query strings end up in access logs and browser history.
- Never print a whole
boto3response oros.environat DEBUG.
def mask(s: str, keep: int = 4) -> str:
"""Last 4 characters only, for log lines."""
return "*" * max(0, len(s) - keep) + s[-keep:] if s else ""
Files and Paths
from pathlib import Path
# NO: "../../etc/passwd" escapes the directory
target = base / user_supplied_name
# YES: resolve, then prove it is still inside
target = (base / user_supplied_name).resolve()
if not target.is_relative_to(base.resolve()): # 3.9+
raise ValueError("path escapes the base directory")
import tempfile
with tempfile.NamedTemporaryFile(delete=False) as fh: # not /tmp/fixed-name
fh.write(data)
os.chmod(path, 0o600) # a secret on disk is owner-only
tempfile picks an unpredictable name; a fixed /tmp/myapp.json is a symlink attack waiting for a multi-user host.
Gain comprehensive insights from Bash sed & awk Cheatsheet
Dependencies
pip install pip-audit && pip-audit # known CVEs in the environment
pip install --require-hashes -r requirements.txt
pip download --no-deps pkg && sha256sum pkg-*.whl
- Pin versions in
requirements.txt; a range means a different build tomorrow. - Check the package name before installing. Typosquats (
python-dateutilvsdateutil) are a live supply-chain route. pip installruns arbitrary code fromsetup.pyat install time, so this is not a theoretical risk.
A Quick Audit of Your Own Code
grep -rn "shell=True\|yaml.load(\|verify=False\|pickle.load\|eval(" --include=*.py .
pip install bandit pip-audit
bandit -r . -ll # medium severity and above, exits non-zero
pip-audit --strict # known CVEs in the resolved environment
# .github/workflows/security.yml
- run: pip install bandit pip-audit
- run: bandit -r . -ll
- run: pip-audit --strict
Bandit finds the patterns on this sheet by rule id: B602 (shell=True),
B506 (yaml.load), B301 (pickle), B303 (weak hashes), B501
(verify=False). Silence a checked false positive inline with
# nosec B602 - argument is an allow-listed constant, never project-wide.
grep is four of the five rules on this sheet in one line. It is worth putting in CI before it is worth putting in a code review checklist.Common Pitfalls
assertfor validation. Assertions are stripped bypython -O, so a security check written asassert user.is_admindisappears in an optimised run. Useif ... raise.- Catching
Exceptionaround a permission check, turning a denial into a silent pass. tarfile.extractall()on an archive you did not build — entries can carry../paths. Usefilter="data"(3.12+) or validate each member.- Debug mode in production. A framework traceback page prints source, locals and often environment variables.
input()in an automation script, which blocks forever in CI and is a prompt nobody sees.
Run that grep against the repository you deployed most recently. If it returns a line, do you know who controls the value being interpolated?
Deepen your understanding in Bash Basics Cheatsheet
Related Cheatsheets
- Python Logging Cheatsheet — where secrets most often leak
- Python Boto3 Cheatsheet — Secrets Manager, SSM and IAM from Python
- Python Basics & CLI Cheatsheet — the syntax underneath this
Similar Articles
Related Content
More from development
Python basics and CLI cheatsheet: core syntax, f-strings, comprehensions, pathlib, venv, pip, …
Quick-reference Bash basics cheatsheet: variables, parameter expansion, conditionals, loops, arrays, …
You Might Also Like
Python threading and concurrency cheatsheet: Thread, Lock, Queue, ThreadPoolExecutor, …
Boto3 cheatsheet for AWS automation: sessions, S3, EC2, Lambda, DynamoDB, SSM, presigned URLs, …
Knowledge Quiz
Test your general knowledge with this quick quiz!
A set of multiple-choice questions to test your knowledge.
Take as much time as you need.
Your score will be shown at the end.
Question 1 of 5
Quiz Complete!
Your score: 0 out of 5
Loading next question...

