1. Introduction
In modern software development, shipping features fast often comes at the cost of security. Developers write hundreds of lines of code per day, and subtle vulnerabilities can slip through unnoticed — until an attacker finds them first.
This is exactly where SAST (Static Application Security Testing) tools come in. Unlike dynamic analysis, SAST tools scan your source code without executing it, catching security flaws early in the development cycle before they ever reach production.
In this article, I’ll walk you through applying Bandit — one of the most popular open-source SAST tools for Python — to the backend of a real web vulnerability scanner application built with FastAPI. I’ll show you the exact commands I ran, the findings Bandit reported, and how I fixed the most critical vulnerability.
2. Tool Overview: What is Bandit?
Bandit is an open-source SAST tool developed by the PyCQA (Python Code Quality Authority) community. It was originally created at OpenStack and is now one of the most widely used security linters for Python projects.
How it works:
Bandit parses each Python file into an Abstract Syntax Tree (AST) and runs a set of security-focused plugins against the tree nodes. Each plugin maps to a specific vulnerability class (e.g., hardcoded passwords, insecure SSL, dangerous function calls).
Key facts:
- 🐍 Language: Python only
- 📦 Install:
pip install bandit - 📄 Output formats: TXT, JSON, HTML, XML, CSV, SARIF
- 🔌 IDE integration: VS Code, PyCharm (via plugins)
- ✅ CI/CD ready: GitHub Actions, GitLab CI, Jenkins
- 📋 License: Apache 2.0 (free and open-source)
Vulnerability categories covered:
- Hardcoded credentials and secrets
- Insecure use of cryptography
- SQL injection patterns
- Dangerous function calls (
eval,exec,os.system) - SSL/TLS misconfigurations
- Insecure temp file usage
- Use of
assertin production code
3. Target Application
The application analyzed is VulnScan Pro, a web vulnerability scanner platform built as part of a university security engineering project (Course: SI784 – 2026).
Stack:
- Backend: Python 3.12 + FastAPI
- Database: SQLite (development) / MySQL (production)
- Authentication: JWT + bcrypt
- AI Integration: DeepSeek AI for vulnerability analysis
The backend contains approximately 2,410 lines of Python code spread across these key modules:
File Purposemain.py
FastAPI app setup, CORS, middleware
auth.py
JWT authentication, bcrypt hashing
scanner.py
Core vulnerability scanning engine (737 lines)
models.py
SQLAlchemy database models
routes/
API route handlers
vulnerable_app.py
Intentionally vulnerable test application
4. Installation & Setup
Installing Bandit is straightforward with pip:
# Install Bandit
pip install bandit
# Verify installation
bandit --version
# bandit 1.9.4
Enter fullscreen mode Exit fullscreen mode
Optional: Create a Bandit configuration file (.bandit) to skip test directories:
[bandit]
exclude_dirs = ['tests', '__pycache__']
skips = ['B101']
Enter fullscreen mode Exit fullscreen mode
Tip: You can also install Bandit as a pre-commit hook to catch issues before every commit:
# .pre-commit-config.yaml repos: - repo: https://github.com/PyCQA/bandit rev: 1.9.4 hooks: - id: bandit args: ["-ll"]
5. Running the Analysis
I ran Bandit against the entire backend directory using the following commands:
Full scan (all severity levels):
bandit -r ./backend -f txt
Enter fullscreen mode Exit fullscreen mode
Scan targeting medium and high severity only:
bandit -r ./backend -f txt --severity-level medium
Enter fullscreen mode Exit fullscreen mode
Export as HTML report:
bandit -r ./backend -f html -o bandit_report.html
Enter fullscreen mode Exit fullscreen mode
Real output summary from the scan:
Run metrics:
Total issues (by severity):
Undefined: 0
Low: 55
Medium: 1
High: 2
Total issues (by confidence):
Undefined: 0
Low: 0
Medium: 3
High: 55
Total lines of code: 2,410
Total lines skipped (#nosec): 0
Files skipped: 0
Enter fullscreen mode Exit fullscreen mode
Bandit scanned 2,410 lines of Python code across all backend files and found 58 total issues in under 2 seconds: 2 High, 1 Medium, and 55 Low severity findings.
6. Results & Findings
Summary Table
# Severity Rule ID Description File OWASP Top 10 1 🔴 HIGH B501 SSL cert verification disabledscanner.py:31
A02 – Cryptographic Failures
2
🔴 HIGH
B501
SSL cert verification disabled
scanner.py:610
A02 – Cryptographic Failures
3
🟡 MEDIUM
B108
Insecure temp directory /tmp
report_routes.py:17
A05 – Security Misconfiguration
4-58
🟢 LOW
B101
assert used in test code
tests/*.py
—
🔴 Finding #1 — HIGH — SSL Certificate Verification Disabled
Rule: B501 request_with_no_cert_validation
File: scanner.py, lines 31 and 610
OWASP Top 10: A02:2021 – Cryptographic Failures
CWE: CWE-295 — Improper Certificate Validation
# VULNERABLE CODE — scanner.py, lines 24-35
def safe_get(url: str, timeout: int = TIMEOUT, allow_redirects: bool = True, **kwargs):
try:
return requests.get(
url,
timeout=timeout,
headers=HEADERS_UA,
allow_redirects=allow_redirects,
verify=False, # ← THIS IS THE PROBLEM
**kwargs
)
except RequestException:
return None
Enter fullscreen mode Exit fullscreen mode
verify=False completely disables SSL certificate validation. This means the scanner itself is vulnerable to Man-in-the-Middle (MITM) attacks — an attacker intercepting traffic between the scanner and a target site could inject malicious responses, causing the scanner to report false results or leak sensitive scan data.
🟡 Finding #2 — MEDIUM — Insecure Temp Directory Usage
Rule: B108 hardcoded_tmp_directory
File: routes/report_routes.py, line 17
OWASP Top 10: A05:2021 – Security Misconfiguration
CWE: CWE-377 — Insecure Temporary File
# VULNERABLE CODE — report_routes.py, line 17
REPORTS_DIR = os.getenv("REPORTS_DIR", "/tmp/vulnscan_reports")
Enter fullscreen mode Exit fullscreen mode
Using /tmp as a fallback for storing security reports is a risk. On shared Linux systems, /tmp is world-readable, meaning other processes could read generated vulnerability reports containing sensitive scan results about target systems.
🟢 Findings #4-58 — LOW — assert in Test Code
Rule: B101 assert_used
Files: tests/test_scanner.py
CWE: CWE-703
All 55 Low-severity findings are assert statements in the test suite — which is standard Python testing practice. These are false positives in this context and can be suppressed by configuring Bandit to skip the tests/ directory.
7. Fixing the Critical Vulnerability
Fixing B501 — SSL Certificate Verification
Before (Vulnerable):
def safe_get(url: str, timeout: int = TIMEOUT, allow_redirects: bool = True, **kwargs):
try:
return requests.get(
url,
timeout=timeout,
headers=HEADERS_UA,
allow_redirects=allow_redirects,
verify=False, # SSL verification disabled — MiTM risk!
**kwargs
)
except RequestException:
return None
Enter fullscreen mode Exit fullscreen mode
After (Fixed):
import certifi
def safe_get(url: str, timeout: int = TIMEOUT, allow_redirects: bool = True, **kwargs):
"""
Safe HTTP GET with proper SSL verification using Mozilla's CA bundle.
"""
try:
return requests.get(
url,
timeout=timeout,
headers=HEADERS_UA,
allow_redirects=allow_redirects,
verify=certifi.where(), # Use trusted Mozilla CA bundle
**kwargs
)
except requests.exceptions.SSLError:
# SSL error — log it, don't silently ignore
return None
except RequestException:
return None
Enter fullscreen mode Exit fullscreen mode
# Install certifi
pip install certifi
# Re-run Bandit to confirm the fix
bandit scanner.py --severity-level medium
# Result: No issues identified. ✅
Enter fullscreen mode Exit fullscreen mode
Why
certifi.where()? Thecertifipackage provides Mozilla’s curated collection of root certificates, ensuring reliable SSL validation across all platforms without depending on the OS certificate store.
Fixing B108 — Temp Directory
# BEFORE
REPORTS_DIR = os.getenv("REPORTS_DIR", "/tmp/vulnscan_reports")
# AFTER — use an application-specific directory with restricted permissions
import tempfile, pathlib
DEFAULT_REPORTS_DIR = pathlib.Path(__file__).parent / "reports"
REPORTS_DIR = pathlib.Path(os.getenv("REPORTS_DIR", str(DEFAULT_REPORTS_DIR)))
REPORTS_DIR.mkdir(mode=0o700, parents=True, exist_ok=True) # Owner-only permissions
Enter fullscreen mode Exit fullscreen mode
8. Pros & Cons of Bandit
✅ Strengths
Feature Detail Zero configuration Works instantly withpip install bandit
Extremely fast
Analyzed 2,410 LOC in under 2 seconds
SARIF output
Native GitHub Code Scanning integration
Granular control
Skip specific rules, files, or directories
Educational
Every finding links to CWE, OWASP, and detailed docs
⚠️ Limitations
Limitation Impact Python only Cannot analyze JS, TypeScript, Go, etc. High Low-severity noise 55 of 58 findings were testassert statements
No data flow tracking
Cannot follow tainted data across function calls
No dependency analysis
Won’t catch CVEs in requirements.txt packages
No runtime context
May flag dead code or unreachable paths
Effective false positive rate: ~95% of Low findings in this scan were test-file noise. With a proper .bandit config, the actionable issue count drops to exactly 3 real findings.
9. Conclusion
Applying Bandit to VulnScan Pro’s FastAPI backend in under 5 minutes revealed 3 real security issues in 2,410 lines of code — most critically, an SSL certificate verification bypass (verify=False) in the core scanning engine that could expose the tool to MITM attacks.
The irony here is significant: a security scanner was itself vulnerable to the kind of attack it was designed to detect. This underscores why SAST is essential even for security-focused projects.
Key lessons from this analysis:
-
verify=Falseinrequestsis a red flag — always usecertifior your system CA bundle - Never use
/tmpas a default for sensitive data without explicit permission restrictions - Configure Bandit to skip test directories to eliminate false positive noise
For any Python project, adding Bandit to your CI/CD pipeline takes less than 10 minutes and can catch critical security issues automatically on every push:
# .github/workflows/sast.yml
- name: Run Bandit SAST
run: bandit -r ./backend --severity-level medium -f sarif -o bandit.sarif
- name: Upload SARIF
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: bandit.sarif
Enter fullscreen mode Exit fullscreen mode
SAST is not a silver bullet, but it is the cheapest and fastest layer of your DevSecOps defense-in-depth strategy. Ship secure code — scan before you push.
Written by Mariela Ramos · SI784 Security Engineering · 2026
Tools: Bandit 1.9.4 · Python 3.12 · FastAPI 0.111 · OWASP Top 10 2021
답글 남기기