Working Code Isn’t Necessarily Secure Code
Many developers assume that once a feature works as expected and tests pass with 100% coverage, their job is done. Reality tells a different story. You might build a door that opens and closes seamlessly, but if you forget the latch or hide the key under the doormat, intruders can easily walk in without ever picking a lock.
In Python projects, silly security oversights happen surprisingly often. A developer hardcodes a database password into a configuration file for quick debugging and forgets to remove it. Another still hashes passwords with MD5 out of habit. Someone else concatenates raw user input straight into os.system(). When reviewing tens of thousands of lines of code every sprint, human eyes simply cannot catch everything.
According to NIST, fixing a security defect in production costs up to 30 times more than addressing it during development. Waiting until attackers exploit a vulnerability carries an immense price tag. That is why we need automated tools to inspect every line of code directly on our local machines.
Understanding SAST and Bandit
What Is SAST?
SAST stands for Static Application Security Testing. Simply put, it scans your entire codebase without executing the program.
It compares code patterns against a known repository of security vulnerabilities. This approach puts the Shift-Left Security principle into practice, catching flaws directly in the developer’s IDE instead of letting them slip onto production servers.
How Bandit Works
Bandit is an open-source security scanner designed specifically for the Python ecosystem. Rather than merely searching for string matches with basic regular expressions, Bandit parses source code into an Abstract Syntax Tree (AST). By understanding the syntactic context, Bandit executes plugins that check for common security issues:
- B102 / B307 (eval/exec): Executing arbitrary strings, opening the door to Remote Code Execution (RCE).
- B602 / B605 (subprocess/shell injection): Invoking system commands with
shell=True. - B105 / B106 (hardcoded secrets): Exposing passwords, API tokens, or private keys in code.
- B303 / B324 (insecure crypto): Using weak hashing algorithms like MD5 or SHA1 for security purposes.
Hands-On: Installation, Code Scanning, and Building a CI/CD Pipeline
Step 1: Install Bandit
Install the Bandit package directly via pip in your virtual environment:
pip install bandit
Step 2: Create a Vulnerable Sample Script
Create a file named demo_app.py containing the following three classic flaws:
import os
import hashlib
# Issue 1: Hardcoded admin password directly in the code
ADMIN_PASSWORD = "SuperSecretPassword123!"
# Issue 2: Using the broken MD5 hashing algorithm
def hash_password(pwd: str) -> str:
return hashlib.md5(pwd.encode('utf-8')).hexdigest()
# Issue 3: String concatenation in shell commands, vulnerable to Command Injection
def ping_host(host_ip: str) -> None:
cmd = "ping -c 1 " + host_ip
os.system(cmd)
if __name__ == "__main__":
print("Hashed:", hash_password("test"))
# An attacker can inject strings such as: 127.0.0.1; rm -rf / or whoami
ping_host("127.0.0.1; whoami")
Step 3: Run the Code Scan
To scan the single file you just created:
bandit demo_app.py
When scanning an entire project directory (such as the src/ folder), add the recursive flag -r:
bandit -r src/
Bandit will analyze the code and output detailed findings with line numbers, severity ratings (Severity: Low, Medium, High), and confidence levels (Confidence):
- Line 5: Flags
[B105:hardcoded_password_string](Severity: Low, Confidence: Medium). - Line 9: Flags
[B303:md5](Severity: Medium, Confidence: High). - Line 14: Flags
[B605:start_process_with_a_shell](Severity: High, Confidence: High).
Handling Exceptions and Validating Input
To resolve the Command Injection issue in the ping_host function, validate the IP address format before executing the subprocess. Regular expressions (Regex) are the standard solution here.
If you need to quickly test your regex pattern before adding it to your Python code, you can use the regex tester at toolcraft.app/en/tools/developer/regex-tester. It runs right in the browser with no installation needed, letting you verify that your pattern matches both IPv4 and IPv6 addresses correctly.
If you have a legitimate use case that requires a flagged function but is completely safe (for instance, using MD5 purely to verify a downloaded file’s checksum rather than hashing passwords), add a # nosec comment at the end of the line so Bandit skips it:
# Only computing a temporary file checksum, not used for security
checksum = hashlib.md5(file_chunk).hexdigest() # nosec B303
Step 4: Configure Rules via bandit.yaml
In production projects, you should place a bandit.yaml file in the root directory to manage rules and exclude test suites or virtual environment directories:
# Skip B101 (assert_used) since assert is commonly used in test files
skips: ['B101']
exclude_dirs:
- 'tests'
- 'venv'
- '.venv'
Command to run with the configuration file:
bandit -c bandit.yaml -r .
Step 5: Automate with GitHub Actions
Relying on manual discipline is never enough in a team setting. The most effective approach is establishing an automated quality gate in your CI/CD pipeline: any Pull Request introducing security issues of Medium severity or higher is blocked from merging.
Create the file .github/workflows/security-scan.yml with the following content:
name: Python Security Scan
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
bandit-scan:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Set up Python 3.11
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install Bandit
run: pip install bandit
- name: Run Bandit Scan
# Flag -ll: Only fail build on Medium severity or higher
# Flag -ii: Only consider warnings with Medium confidence or higher
# Flag -x: Exclude directories from scanning
run: bandit -r . -ll -ii -x ./tests,./venv
Whenever a new commit or Pull Request is pushed, GitHub Actions triggers the runner, checks out the code, and executes Bandit. If a serious vulnerability is found, the job exits with a non-zero status code and fails the build. Your main branch remains continuously protected around the clock.
Conclusion
Security is not an obstacle that slows down development; it is an essential part of clean code standards. Integrating Bandit into your daily workflow takes just ten minutes of setup, yet it catches the vast majority of common security blunders long before your code ever reaches real users.
