Finding Security Vulnerabilities with Bandit
What Bandit checks
Section titled “What Bandit checks”Bandit scans for patterns like:
- use of
eval subprocesscalls without safety- hardcoded passwords
- insecure temporary files
bandit -r your_packageExample finding
Section titled “Example finding”# Bandit will warn about eval usage
user_input = "2 + 2"
result = eval(user_input)- Treat Bandit warnings as “review required”, not always “bug”.
- Combine with dependency scanning in CI.
One file, six tools
Section titled “One file, six tools”Every measurement on this page — and on the other five tools in this phase — comes from running the tool against this deliberately flawed file:
import os
import subprocess
import hashlib
def process(items, user_input, flag = False):
unused_var = 42
result=[]
for i in items:
if i > 0:
if flag:
if i % 2 == 0:
result.append(i*2)
else:
result.append(i)
else:
result.append(i)
password = "hunter2"
h = hashlib.md5(password.encode()).hexdigest()
os.system("echo " + user_input)
subprocess.call("ls " + user_input, shell=True)
return result
def add(a: int, b: int) -> int:
return a + b
x = add("1", 2)| tool | what it reported on sample.py |
|---|---|
| flake8 | 5 style and dead-code issues. No security findings. |
| pylint | 4 issues, score 8.10/10 |
| mypy | 1 type error, which neither linter saw |
| bandit | 5 security issues, 3 of them HIGH |
| radon | complexity A (5), maintainability A (56.30) |
The headline is that no tool subsumes another. flake8 read the whole file and reported nothing about the shell injection on line 20. bandit read the same file and said nothing about the type error on line 29. Running one and concluding the code is clean is the mistake this phase exists to prevent.
What bandit found that nothing else did
Section titled “What bandit found that nothing else did”LOW B404 line 2: Consider possible security implications of the subprocess module
LOW B105 line 18: Possible hardcoded password: 'hunter2'
HIGH B324 line 19: Use of weak MD5 hash for security
HIGH B605 line 20: Starting a process with a shell, possible injection detected
HIGH B602 line 21: subprocess call with shell=True identifiedThree HIGH severity findings. flake8 reported five style issues on the same file and none of these. pylint scored it 8.10/10. mypy found an unrelated type error. Only bandit was asking whether the code was dangerous.
flowchart TD B["bandit walks the AST"] --> P["pattern per known weakness"] P --> S1["shell=True with a built string"] P --> S2["weak hashes: md5, sha1"] P --> S3["hardcoded password-like strings"] P --> S4["assert used for control flow"] P --> S5["yaml.load, pickle, eval, exec"] S1 --> R["severity x confidence"]
The three HIGH findings, and why they are HIGH
Section titled “The three HIGH findings, and why they are HIGH”os.system("echo " + user_input) # B605
subprocess.call("ls " + user_input, shell=True) # B602A user sending ; rm -rf ~ runs their command, not yours. The fix removes the shell
entirely — pass a list and no shell is involved, so there is nothing to inject into:
subprocess.run(["ls", user_input], check=True) # no shell, no injectionhashlib.md5(password.encode()).hexdigest() # B324MD5 is fast and broken. For passwords, use a deliberately slow function — scrypt via
werkzeug.security.generate_password_hash, or argon2. For a non-security checksum, say
so explicitly and bandit stops complaining:
hashlib.md5(data, usedforsecurity=False).hexdigest()Severity and confidence are separate axes
Section titled “Severity and confidence are separate axes”bandit -r . -ll # only MEDIUM and HIGH severity
bandit -r . -iii # only HIGH confidence
bandit -r . -ll -ii # a sensible CI gateB404 above is LOW severity — importing subprocess is not a vulnerability, just a hint
to look closer. Gating CI on every finding trains people to ignore the tool; gating on
medium-and-above keeps it credible.
False positives are expected, and should be justified in the code
Section titled “False positives are expected, and should be justified in the code”password = "not-a-real-secret" # nosec B105 - fixture value used only in testsAlways name the test id and give a reason. A bare # nosec disables everything on that
line, including a genuine issue introduced later.
[tool.bandit]
exclude_dirs = ["tests", ".venv"]
skips = ["B101"] # assert_used — asserts are the point in testsCheck yourself
Section titled “Check yourself”-
Bandit reported three HIGH severity issues in a file that flake8 and pylint both passed with only style complaints. What does that tell you?
Measured B324, B605 and B602 as HIGH. pylint scored the same file 8.10/10. Running one tool and concluding the code is clean is the mistake.
pch.quizShowAnswer
B — the tools ask different questions; linters look at style and dead code, not at dangerous patterns — Measured B324, B605 and B602 as HIGH. pylint scored the same file 8.10/10. Running one tool and concluding the code is clean is the mistake.
-
What is the fix for subprocess.call('ls ' + user_input, shell=True)?
subprocess.run(['ls', user_input]) executes the program directly. Escaping and validation are attempts to outguess a shell you do not need.
pch.quizShowAnswer
B — pass a list of arguments and drop shell=True, so no shell is involved and there is nothing to inject into — subprocess.run(['ls', user_input]) executes the program directly. Escaping and validation are attempts to outguess a shell you do not need.
-
Why should CI gate on medium-and-above rather than on every bandit finding?
B404 simply notes that subprocess was imported. Keeping the gate credible is what makes people act on the findings that matter.
pch.quizShowAnswer
B — low findings such as B404 are hints rather than vulnerabilities, and failing on everything trains people to ignore the tool — B404 simply notes that subprocess was imported. Keeping the gate credible is what makes people act on the findings that matter.
-
What does a clean bandit run NOT tell you?
Bandit matches shapes in the AST. A broken ownership check or a logic flaw that leaks one user's data to another is invisible to it.
pch.quizShowAnswer
B — that the application's authorisation logic is correct — Bandit matches shapes in the AST. A broken ownership check or a logic flaw that leaks one user's data to another is invisible to it.
pch.coffeeTagline
pch.coffeeCtapch.feedbackHeading
pch.feedbackSubheading