Skip to content

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#

# Run security scan (recommended)
make security

# Alternative command
make 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:

uv run bandit -r src/ -c pyproject.toml

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:

uv run bandit -r src/ --configfile .bandit

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:

app.run(debug=True)  # Never enable debug in production!

Good:

import os
debug_mode = os.environ.get("FLASK_DEBUG", "False") == "True"
app.run(debug=debug_mode)

B501: Insecure SSL/TLS#

Bad:

requests.get(url, verify=False)  # Disables certificate verification!

Good:

requests.get(url)  # Uses default secure settings
# Or explicitly:
requests.get(url, verify=True)

B506: YAML Load#

Bad:

import yaml
data = yaml.load(user_input)  # Unsafe! Can execute arbitrary code

Good:

import yaml
data = yaml.safe_load(user_input)  # Safe alternative

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)#

# nosec B101
assert user.is_authenticated  # This is safe in our context

Skip in Configuration#

Add to pyproject.toml:

[tool.bandit]
skips = ["B101", "B601"]  # Skip specific test IDs

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#

  1. Run Bandit regularly: Use make security before committing code
  2. Fix HIGH severity issues immediately: These are critical security problems
  3. Review MEDIUM severity issues: Many are real problems worth addressing
  4. Don't blindly skip warnings: Understand why Bandit flagged something before using # nosec
  5. Keep Bandit updated: Run uv add --dev bandit@latest periodically
  6. Use environment variables: Never hardcode secrets, passwords, or API keys
  7. Enable in CI/CD: Already configured in this project
  8. Document exceptions: If you must skip a warning, add a comment explaining why

Makefile Integration#

The project Makefile (Makefile:49-52) includes Bandit:

security: bandit

bandit:
    uv run bandit -r src/ -c pyproject.toml --severity-level medium

Available commands:

make security  # Run Bandit security scan
make bandit    # Alias for make security

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:

# Ensure dev dependencies are installed
make install

# Or manually sync
uv sync --all-groups

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:

[tool.bandit]
exclude_dirs = ["2024-Django-Attempt", ".venv", "tests", "your_dir_here"]

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:

uv run bandit -r docs/hooks

Bandit flagged two issues:

  1. B404 (line 3): Import of subprocess module
  2. Severity: LOW
  3. Confidence: HIGH
  4. Message: "Consider possible security implications associated with the subprocess module."

  5. B603 (line 24): subprocess call without shell=True

  6. Severity: LOW
  7. Confidence: HIGH
  8. 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:

  1. No user input: Both sys.executable (Python interpreter path) and script_path (hardcoded file path) are controlled values
  2. List format used: Using list format [sys.executable, str(script_path)] instead of shell string prevents shell injection
  3. No shell=True: Not using shell=True prevents command injection vulnerabilities
  4. Hardcoded path: script_path = Path("scripts/generate-req-index.py") is a literal string, not user-provided input
  5. 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#

  1. Not all Bandit warnings are real vulnerabilities - LOW severity warnings often require context
  2. Always investigate before suppressing - Understand WHY Bandit flagged something
  3. Document your reasoning - Use descriptive # nosec comments explaining why it's safe
  4. Consider the context:
  5. Is the input controlled or user-provided?
  6. Is the code path accessible to attackers?
  7. 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:

  1. Excluded the directory in pyproject.toml:

    [tool.bandit]
    exclude_dirs = ["docs/hooks"]  # Skip entire directory
    
    Rejected: Too broad - we want security checks on hooks

  2. Skipped the test globally in pyproject.toml:

    [tool.bandit]
    skips = ["B404", "B603"]  # Skip subprocess warnings everywhere
    
    Rejected: Would miss real subprocess vulnerabilities in other files

  3. Used inline suppression (chosen approach):

    # nosec B603 - Explanation here
    
    Selected: Surgical approach, documents reasoning, maintains security checks elsewhere

Testing the Fix#

After adding # nosec comments, verify Bandit no longer reports these issues:

uv run bandit -r docs/hooks -f json -o bandit-docs-report.json

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:

UnicodeEncodeError: 'charmap' codec can't encode character '\u2705'

Problem: Windows console (cmd.exe) uses cp1252 encoding which doesn't support emoji/Unicode characters that Bandit tries to output.

Solutions:

  1. Use JSON output:

    uv run bandit -r src/ -f json -o bandit-report.json
    

  2. Use PowerShell (better Unicode support):

    $env:PYTHONIOENCODING="utf-8"
    uv run bandit -r src/
    

  3. Set environment variable in cmd.exe:

    set PYTHONIOENCODING=utf-8
    uv run bandit -r src/
    

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:

API_KEY = "sk-1234567890"  # gitleaks:allow - Example code for documentation

Solutions:

  1. Use gitleaks:allow comment (keeps realistic examples):

    API_KEY = "sk-1234567890"  # gitleaks:allow - Example for documentation
    SECRET = "real-looking-key"  # gitleaks:allow - Tutorial example only
    

  2. Use placeholder values (avoids scanner triggers):

    API_KEY = "your-api-key-here"  # Safe - won't trigger scanners
    SECRET_TOKEN = "example-hardcoded-value"  # Safe - clearly a placeholder
    

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#