Bandit Security Guide#
This project uses Bandit to identify common security issues in Python code. Bandit performs static analysis to find security vulnerabilities before they reach production.
What is Bandit?#
Bandit is a security linter designed to find common security issues in Python code. It analyzes your code for patterns that could lead to security vulnerabilities, such as:
- SQL injection vulnerabilities
- Hardcoded passwords and secrets
- Insecure cryptographic functions
- Shell injection vulnerabilities
- Insecure deserialization
- And many more...
Quick Start#
Running Bandit#
Manual Usage#
# Scan src/ directory with project configuration
uv run bandit -r src/ -c pyproject.toml
# Scan with severity filter (medium and high only)
uv run bandit -r src/ --severity-level medium
# Generate JSON report
uv run bandit -r src/ -f json -o bandit-report.json
# Scan specific file
uv run bandit src/module/file.py
Configuration#
This project has Bandit configured in two places. You can use either one:
Option 1: pyproject.toml (Active Configuration)#
Located in pyproject.toml:53-60:
[tool.bandit]
exclude_dirs = ["2024-Django-Attempt", ".venv", "tests", "migrations", ".git", "docs"]
skips = ["B101"] # Skip assert_used check (common in tests)
severity = ["medium", "high"] # Only report medium and high severity
confidence = ["medium", "high"] # Only report medium and high confidence
To use this configuration:
Option 2: .bandit YAML File (Alternative)#
Located in .bandit at project root. This is an alternative configuration format with more detailed comments.
To use this configuration:
Understanding Bandit Output#
Severity Levels#
- HIGH: Critical security issues that should be fixed immediately
- MEDIUM: Potential security issues that warrant review
- LOW: Minor issues or potential false positives
Confidence Levels#
- HIGH: Very likely to be a real security issue
- MEDIUM: Could be a security issue, needs review
- LOW: Might be a false positive
Example Output#
>> Issue: [B105:hardcoded_password_string] Possible hardcoded password: 'my_secret_key'
Severity: Low Confidence: Medium
Location: src/config/settings.py:15
More Info: https://bandit.readthedocs.io/en/latest/plugins/b105_hardcoded_password_string.html
14
15 SECRET_KEY = 'my_secret_key' # SECURITY ISSUE!
16
Common Security Issues Detected#
B105: Hardcoded Passwords#
Bad:
password = "admin123" # Never hardcode passwords!
API_KEY = "your-api-key-here" # Never hardcode API keys!
SECRET_TOKEN = "hardcoded-secret-value" # Never hardcode secrets!
Good:
import os
password = os.environ.get("DB_PASSWORD")
API_KEY = os.environ.get("API_KEY")
SECRET_TOKEN = os.environ.get("SECRET_TOKEN")
B201: Flask Debug Mode#
Bad:
Good:
B501: Insecure SSL/TLS#
Bad:
Good:
B506: YAML Load#
Bad:
Good:
B608: SQL Injection#
Bad:
query = f"SELECT * FROM users WHERE name = '{user_input}'"
cursor.execute(query) # SQL injection vulnerability!
Good:
query = "SELECT * FROM users WHERE name = %s"
cursor.execute(query, (user_input,)) # Parameterized query
Skipping False Positives#
Sometimes Bandit flags code that is actually safe. You can skip specific warnings:
Skip Inline (Use Sparingly)#
Skip in Configuration#
Add to pyproject.toml:
Integration with Development Workflow#
Pre-commit Hooks (Optional)#
If you enable pre-commit hooks (.pre-commit-config.yaml), Bandit will run automatically before each commit:
# Install pre-commit (if not already installed)
uv add --dev pre-commit
# Install the git hooks
uv run pre-commit install
# Now Bandit runs automatically on git commit
git commit -m "Your message"
CI/CD Pipeline#
Bandit runs automatically in GitHub Actions via .github/workflows/security.yml:34-44 on every push and pull request.
Best Practices#
- Run Bandit regularly: Use
make securitybefore committing code - Fix HIGH severity issues immediately: These are critical security problems
- Review MEDIUM severity issues: Many are real problems worth addressing
- Don't blindly skip warnings: Understand why Bandit flagged something before using
# nosec - Keep Bandit updated: Run
uv add --dev bandit@latestperiodically - Use environment variables: Never hardcode secrets, passwords, or API keys
- Enable in CI/CD: Already configured in this project
- Document exceptions: If you must skip a warning, add a comment explaining why
Makefile Integration#
The project Makefile (Makefile:49-52) includes Bandit:
Available commands:
Common Bandit Test IDs#
| ID | Name | Description |
|---|---|---|
| B101 | assert_used | Use of assert (disabled for tests) |
| B105 | hardcoded_password_string | Hardcoded password string |
| B106 | hardcoded_password_funcarg | Hardcoded password function argument |
| B107 | hardcoded_password_default | Hardcoded password default |
| B201 | flask_debug_true | Flask app with debug=True |
| B301 | pickle | Use of pickle (unsafe deserialization) |
| B303 | md5 | Use of insecure MD5 hash |
| B304 | ciphers | Use of insecure ciphers |
| B501 | request_with_no_cert_validation | SSL certificate verification disabled |
| B502 | ssl_with_bad_version | SSL/TLS protocol version |
| B506 | yaml_load | Use of yaml.load() |
| B608 | hardcoded_sql_expressions | Possible SQL injection |
Full list: Bandit Plugins Documentation
Troubleshooting#
Issue: "Bandit not found"#
Solution:
Issue: Too many false positives#
Solution: Adjust severity/confidence levels in pyproject.toml:
[tool.bandit]
severity = ["high"] # Only show high severity
confidence = ["high"] # Only show high confidence
Issue: Need to exclude specific directories#
Solution: Add to exclude_dirs in pyproject.toml:
Lessons Learned from This Project#
This section documents real security findings from running Bandit on this project and how we resolved them.
Case Study: False Positives in MkDocs Hooks#
Date: 2025-12-29 File: docs/hooks/generate_req_index.py Issues Found: B404, B603
The Findings#
When running Bandit on documentation hooks:
Bandit flagged two issues:
- B404 (line 3): Import of subprocess module
- Severity: LOW
- Confidence: HIGH
-
Message: "Consider possible security implications associated with the subprocess module."
-
B603 (line 24): subprocess call without shell=True
- Severity: LOW
- Confidence: HIGH
- Message: "subprocess call - check for execution of untrusted input."
The Code#
import subprocess # Line 3 - B404 flagged
import sys
from pathlib import Path
def on_pre_build(config):
# script_path = Path("scripts/generate-req-index.py") # Original
script_path = Path("scripts/generate-req-index_UPDATE.py") # TEMP: Testing updated version
if script_path.exists():
try:
# Line 24 - B603 flagged
subprocess.run([sys.executable, str(script_path)], check=True)
except subprocess.CalledProcessError as e:
print(f"Failed: {e}")
Why These Are False Positives#
These warnings are false positives because the code is actually secure:
- No user input: Both
sys.executable(Python interpreter path) andscript_path(hardcoded file path) are controlled values - List format used: Using list format
[sys.executable, str(script_path)]instead of shell string prevents shell injection - No shell=True: Not using
shell=Trueprevents command injection vulnerabilities - Hardcoded path:
script_path = Path("scripts/generate-req-index.py")is a literal string, not user-provided input - Controlled execution: The script only runs during MkDocs build process on trusted local files
The Resolution#
We suppressed these warnings using # nosec with detailed explanations:
import subprocess # nosec B404 - subprocess used safely with hardcoded, controlled inputs only
import sys
from pathlib import Path
def on_pre_build(config):
# script_path = Path("scripts/generate-req-index.py") # Original
script_path = Path("scripts/generate-req-index_UPDATE.py") # TEMP: Testing updated version
if script_path.exists():
try:
# nosec B603 - script_path is hardcoded, sys.executable is controlled, no user input
subprocess.run([sys.executable, str(script_path)], check=True)
except subprocess.CalledProcessError as e:
print(f"Failed: {e}")
Key Takeaways#
- Not all Bandit warnings are real vulnerabilities - LOW severity warnings often require context
- Always investigate before suppressing - Understand WHY Bandit flagged something
- Document your reasoning - Use descriptive
# noseccomments explaining why it's safe - Consider the context:
- Is the input controlled or user-provided?
- Is the code path accessible to attackers?
- Are there safer alternatives?
When to Suppress vs. Fix#
Suppress with # nosec when: - ✅ You've verified there's no actual security risk - ✅ The input is hardcoded or from trusted sources - ✅ The code is not accessible to untrusted users - ✅ You document WHY it's safe
Fix the code when: - ❌ User input is involved - ❌ External/untrusted data is processed - ❌ The code runs with elevated privileges - ❌ The vulnerability is MEDIUM or HIGH severity
Alternative Approaches Considered#
We could have also:
-
Excluded the directory in
❌ Rejected: Too broad - we want security checks on hookspyproject.toml: -
Skipped the test globally in
❌ Rejected: Would miss real subprocess vulnerabilities in other filespyproject.toml: -
Used inline suppression (chosen approach):
✅ Selected: Surgical approach, documents reasoning, maintains security checks elsewhere
Testing the Fix#
After adding # nosec comments, verify Bandit no longer reports these issues:
Result: No issues reported for these lines, Bandit still scans the rest of the code.
Handling Windows Unicode Encoding Issues#
When running Bandit on Windows, you may encounter:
Problem: Windows console (cmd.exe) uses cp1252 encoding which doesn't support emoji/Unicode characters that Bandit tries to output.
Solutions:
-
Use JSON output:
-
Use PowerShell (better Unicode support):
-
Set environment variable in cmd.exe:
GitLeaks Flagging Documentation Examples#
Date: 2025-12-29 Issue: GitLeaks (secret scanner) flagged example code in this tutorial
Problem: When demonstrating security anti-patterns in documentation, example API keys/secrets can trigger secret scanners:
Solutions:
-
Use gitleaks:allow comment (keeps realistic examples):
-
Use placeholder values (avoids scanner triggers):
Key Takeaways: - For documentation/tutorials: Use gitleaks:allow to keep realistic examples - For actual code: Never use gitleaks:allow - fix the real security issue - Why it matters: Realistic examples help developers recognize actual vulnerabilities - Document your choice: Always add a comment explaining why you're allowing it
Further Reading#
- Official Bandit Documentation
- Bandit Plugins Reference
- Python Security Best Practices
- OWASP Top 10
- CodeGuard Security Framework (integrated in this project)
Related Documentation#
- Makefile Guide - Using make commands
- Dependency Management - Managing dependencies with uv
- Django Secrets Management - Secure configuration practices
- CodeGuard Plugin - AI-assisted security practices