DEV Community

DevOps Fundamental for DevOps Fundamentals

Posted on

Python Fundamentals: bandit

Bandit: A Production Deep Dive into Python's Static Analysis Tool

Introduction

In late 2022, a critical vulnerability was discovered in our internal data pipeline. A seemingly innocuous function responsible for deserializing configuration files from a third-party vendor was susceptible to arbitrary code execution. The root cause? A missing security check allowing the injection of malicious Python code during deserialization. While the immediate impact was contained by network segmentation, the incident highlighted a significant gap in our security tooling. We’d relied heavily on dynamic testing and code review, but lacked a robust static analysis solution integrated into our CI/CD pipeline. This led us to deeply investigate and fully adopt bandit, a tool that has since become integral to our development workflow. This post details our journey, focusing on practical implementation, performance considerations, and the pitfalls we’ve encountered.

What is "bandit"?

bandit is a static analysis tool for finding common security issues in Python code. It’s not a linter in the traditional sense (though it overlaps), but rather a security-focused scanner. It operates by analyzing the Abstract Syntax Tree (AST) of Python code, identifying patterns known to be associated with vulnerabilities. It’s maintained by the Python Security Association and is not governed by a formal PEP, but its design aligns with Python’s emphasis on readability and maintainability. Unlike tools like pylint, bandit doesn’t focus on style or code quality; its sole purpose is to identify potential security flaws. It leverages a ruleset, which can be customized, to detect issues like hardcoded passwords, insecure use of eval(), and improper input validation.

Real-World Use Cases

  1. API Request Handling (FastAPI): We use bandit to scan our FastAPI endpoints for potential injection vulnerabilities. Specifically, it flags uses of eval() or exec() within request handlers, which are immediate red flags. This is crucial as our API processes user-supplied data.
  2. Async Job Queues (Celery/Dramatiq): Our asynchronous task queues often involve deserializing data from Redis or RabbitMQ. bandit helps identify potential vulnerabilities in the deserialization logic, preventing malicious payloads from executing arbitrary code within our worker processes.
  3. Type-Safe Data Models (Pydantic): While Pydantic provides runtime validation, bandit complements this by identifying potential vulnerabilities in how data is processed after validation. For example, it can flag insecure uses of string formatting with user-provided data, even if Pydantic has already ensured the data conforms to a specific type.
  4. CLI Tools: Our internal CLI tools often handle sensitive configuration files. bandit ensures these tools don’t inadvertently expose secrets or allow for command injection.
  5. ML Preprocessing: Data preprocessing pipelines in our machine learning infrastructure often involve loading data from external sources. bandit helps identify potential vulnerabilities in the data loading and transformation logic, preventing malicious data from compromising our models or systems.

Integration with Python Tooling

Our pyproject.toml config integrates bandit with our existing tooling:

[tool.bandit]
exclude = [
    "tests/",
    ".venv/",
    "migrations/",
]
profiles = [
    "owasp",
    "bandit.plugins.requests.RequestPlugin", # Custom plugin for our API framework

]
skip_b101 = true # Suppress hardcoded password checks (handled by secret management)

Enter fullscreen mode Exit fullscreen mode

We use a custom bandit plugin to extend its functionality for our specific API framework (based on Requests). This plugin adds rules tailored to identify vulnerabilities specific to our API interactions. We also leverage mypy for static type checking, and pytest for unit and integration tests. bandit runs as part of our pre-commit hooks and CI pipeline, failing builds if any security issues are detected. We use a custom runtime hook to dynamically load and apply the custom plugin during CI execution.

Code Examples & Patterns

Here's an example of code flagged by bandit and the corrected version:

Vulnerable Code:

def process_user_input(user_input):
    """Processes user input (INSECURE!)."""
    command = f"echo {user_input}"
    import subprocess
    subprocess.run(command, shell=True)
Enter fullscreen mode Exit fullscreen mode

Secure Code:

import subprocess

def process_user_input(user_input):
    """Processes user input (SECURE)."""
    command = ["echo", user_input]
    subprocess.run(command)
Enter fullscreen mode Exit fullscreen mode

bandit flags the first example (B605 - subprocess.run with shell=True) because it's vulnerable to command injection. The second example avoids this by passing the command as a list, preventing shell interpretation of the user input. We favor using dataclasses for data models, combined with Pydantic for validation, to ensure type safety and reduce the risk of vulnerabilities related to incorrect data handling.

Failure Scenarios & Debugging

One particularly frustrating bug involved a false positive related to a complex data transformation function. bandit flagged it as potentially vulnerable to code injection, but after careful review, it was determined to be a false alarm. Debugging involved using pdb to step through the code and examine the AST generated by bandit. We also used logging to trace the execution flow and verify that the input data was properly sanitized. The root cause was a complex nested conditional statement that bandit misinterpreted as a potential vulnerability. We addressed this by refactoring the code to simplify the logic and adding a more specific exclusion rule to our bandit configuration.

Performance & Scalability

bandit scans can be slow, especially for large codebases. We’ve found that excluding unnecessary directories (e.g., tests/, .venv/) significantly improves performance. We also avoid running bandit on every commit; instead, we run it on pull requests and nightly builds. Profiling with cProfile revealed that the AST parsing phase was the most time-consuming. We’ve experimented with caching the AST to reduce redundant parsing, but the benefits were marginal. The biggest performance gain came from optimizing our custom plugin to avoid unnecessary AST traversals.

Security Considerations

The most significant security risk associated with bandit is relying on its output without critical review. False negatives are possible, and bandit is not a substitute for thorough security testing and code review. Insecure deserialization remains a major concern. We enforce strict input validation and use trusted sources for configuration files. We also employ sandboxing techniques to isolate potentially vulnerable code. We've also seen issues with improper handling of sensitive data in logging statements, which bandit can help identify.

Testing, CI & Validation

Our testing strategy includes:

  • Unit Tests: Verify the correctness of individual functions and modules.
  • Integration Tests: Test the interaction between different components.
  • Property-Based Tests (Hypothesis): Generate random inputs to uncover edge cases and vulnerabilities.
  • Type Validation (mypy): Ensure type safety and prevent runtime errors.
  • Static Checks (bandit): Identify potential security flaws.

Our CI pipeline uses tox to run bandit with different Python versions and configurations. GitHub Actions automatically runs bandit on pull requests and merges. We also use pre-commit hooks to run bandit locally before committing code.

Common Pitfalls & Anti-Patterns

  1. Ignoring Bandit Warnings: Treating bandit warnings as mere suggestions instead of potential security flaws.
  2. Overly Broad Exclusions: Excluding entire directories or files from bandit scans without careful consideration.
  3. Relying Solely on Bandit: Assuming that bandit will catch all security vulnerabilities.
  4. Not Updating Bandit Regularly: Using an outdated version of bandit with known limitations.
  5. Ignoring False Positives: Dismissing false positives without investigating the underlying code.
  6. Complex Suppressions: Using overly complex suppression rules that mask real vulnerabilities.

Best Practices & Architecture

  • Type Safety: Embrace type hints and use mypy to enforce type safety.
  • Separation of Concerns: Design code with clear separation of concerns to reduce complexity and improve maintainability.
  • Defensive Coding: Assume that all input is malicious and validate it accordingly.
  • Modularity: Break down code into small, reusable modules.
  • Config Layering: Use a layered configuration approach to manage different environments.
  • Dependency Injection: Use dependency injection to improve testability and reduce coupling.
  • Automation: Automate everything, from testing to deployment.
  • Reproducible Builds: Ensure that builds are reproducible to prevent supply chain attacks.
  • Documentation: Document code thoroughly to facilitate understanding and maintenance.

Conclusion

bandit is an invaluable tool for improving the security of Python applications. While it’s not a silver bullet, it provides a crucial layer of defense against common vulnerabilities. Mastering bandit requires understanding its limitations, integrating it into your development workflow, and continuously refining your security practices. The incident in 2022 served as a stark reminder that security is not an afterthought; it must be baked into every stage of the development lifecycle. Next steps for your team should include refactoring legacy code to address bandit findings, measuring the performance impact of bandit scans, writing comprehensive tests, and enforcing a linter/type gate in your CI pipeline.

Top comments (0)