← Python Code Performance & Security
Browse Python Concepts

bandit — Static Security Analysis for Python

Mental Model

Imagine Bandit as a diligent security guard who knows all the common weak spots and unsafe practices in building construction. It inspects your blueprints (code) before the building is even put up, pointing out potential flaws like a wall that's too thin or a door with a known faulty lock, without ever needing to run the actual structure.

Rule: Always integrate Bandit into your CI pipeline so security regressions are caught at code review, not in production.

The Setup

A new microservice is being developed, and security checks are intended to be integrated into the CI/CD pipeline. Developers are writing code, and while they are aware of security best practices, human error is inevitable, leading to potential accidental introduction of vulnerabilities.

What Does This Print?

Broken code
Python
# insecure_app.py
import os
import subprocess
import yaml # Often used for configuration
import logging

# Configure logging (good practice)
logging.basicConfig(level=logging.INFO)

def run_command_from_user_input(command: str):
    # BROKEN: Command injection vulnerability (B603: subprocess_without_pipe_capture)
    # Using shell=True is dangerous with user input.
    subprocess.call(command, shell=True) 

def load_config_from_file(filepath: str):
    # BROKEN: Deserialization vulnerability (B506: yaml_load)
    # yaml.unsafe_load is inherently dangerous with untrusted input.
    with open(filepath, 'r') as f:
        config = yaml.unsafe_load(f) # This will definitively trigger B506
    logging.info(f"Configuration loaded: {config}")
    return config

def get_env_variable(key: str):
    # BROKEN: Potential for hardcoded password (B105: hardcoded_password_string)
    # This often gets flagged if a sensitive keyword is in the variable name/value
    if key == "DB_PASSWORD":
        return "hardcoded_secret_123" # Hardcoded sensitive information

    return os.environ.get(key)

if __name__ == '__main__':
    print("Running insecure_app...")
    # Example usage (will trigger vulnerabilities if exploited, but Bandit just scans the code)
    # To test locally: pip install bandit && bandit insecure_app.py
If the 'insecure_app.py' file is scanned with Bandit, which specific security warnings (B-numbers) do you expect it to raise, and why would each be considered a vulnerability?

The Output

What actually happens
[B603:subprocess_without_pipe_capture] subprocess call with shell=True is a security risk, consider using shell=False and a list of arguments. >> Issue: subprocess.call(command, shell=True) [B506:yaml_load] Use of yaml.load() is unsafe. Consider yaml.safe_load(). >> Issue: config = yaml.unsafe_load(f) [B105:hardcoded_password_string] Possible hardcoded password: 'hardcoded_secret_123'. >> Issue: return "hardcoded_secret_123"

Scanning 'insecure_app.py' with Bandit (e.g., bandit insecure_app.py) will identify several common security issues: Each flagged line represents a potential vector for attackers: command injection (B603), arbitrary code execution via deserialization (B506), and exposure of sensitive credentials (B105).

Why Python Does This

Bandit operates by building an Abstract Syntax Tree (AST) of the Python source code, similar to how the Python interpreter processes code. It then applies a set of predefined security rules (plugins) to traverse this AST and identify patterns indicative of security vulnerabilities. For example, the B603 rule looks for calls to subprocess functions where shell=True is set and the command argument is not a hardcoded literal, as this allows arbitrary command injection if the input is untrusted. B506 identifies usage of yaml.load or yaml.unsafe_load because these functions can deserialize arbitrary Python objects, leading to remote code execution if a malicious YAML file is processed. B105 uses string pattern matching to find sensitive keywords near string literals, flagging potential hardcoded secrets. These are not flaws in Python itself but common insecure coding patterns that Bandit is designed to detect through static analysis.

The Fix

Corrected pattern
Python
# secure_app.py
import os
import subprocess
import yaml
import logging

logging.basicConfig(level=logging.INFO)

def run_command_securely(command_args: list):
    # FIX B603: Avoid shell=True with user input; pass commands as a list.
    # subprocess.run is preferred over subprocess.call and 'check=True' will raise errors on non-zero exit codes.
    subprocess.run(command_args, check=True) 

def load_config_securely(filepath: str):
    # FIX B506: Use yaml.safe_load() for untrusted input to prevent arbitrary code execution.
    with open(filepath, 'r') as f:
        config = yaml.safe_load(f) # Use yaml.safe_load for security
    logging.info(f"Configuration loaded securely: {config}")
    return config

def get_env_variable(key: str):
    # FIX B105: Do not hardcode sensitive information. Retrieve from secure environment variables,
    # a secret manager (e.g., Vault, AWS Secrets Manager), or other secure configuration.
    # For demonstration, we simply retrieve from os.environ and provide a safe default.
    if key == "DB_PASSWORD":
        return os.environ.get(key, "safedefault") # Retrieve from env, or provide a safe default.
    return os.environ.get(key)

if __name__ == '__main__':
    print("Running secure_app...")
    # To test locally: pip install bandit && bandit secure_app.py (should report no issues)

Bandit works by building an Abstract Syntax Tree (AST) of your Python code and then running a series of predefined security plugins against it. Each plugin looks for specific patterns (e.g., shell=True in subprocess calls, yaml.unsafe_load, or string literals resembling passwords), flagging them as potential vulnerabilities based on known insecure coding practices.

How This Fails in Real Systems

A Django application handling customer uploads included a utility function that would os.system() user-provided filenames to move them, assuming validated input. A daily Bandit scan, integrated into the CI pipeline, flagged this specific call with a B602 (subprocess_with_shell_equals_true) warning. This immediately alerted the team to a critical command injection vulnerability. Fixing it prevented a scenario where an attacker could upload a file named "image.jpg; rm -rf /" and execute arbitrary commands on the production server, a flaw that was identified and remediated within hours of introduction, thanks to the automated static analysis.

Key Takeaway

Always integrate Bandit into your CI pipeline so security regressions are caught at code review, not in production.
Common mistake: Developers unknowingly introduce common security vulnerabilities by using unsafe functions (e.g., shell=True, yaml.unsafe_load) or hardcoding sensitive information, relying on manual code reviews to catch them.