DEV Community

Securing a FastAPI Backend with Bandit: A Real SAST Analysis

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 assert in 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 Purpose
main.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

Optional: Create a Bandit configuration file (.bandit) to skip test directories:

[bandit]
exclude_dirs = ['tests', '__pycache__']
skips = ['B101']

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

Scan targeting medium and high severity only:

bandit -r ./backend -f txt --severity-level medium

Export as HTML report:

bandit -r ./backend -f html -o bandit_report.html

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

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 disabled scanner.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

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

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

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
# Install certifi
pip install certifi

# Re-run Bandit to confirm the fix
bandit scanner.py --severity-level medium
# Result: No issues identified. โœ…

Why certifi.where()? The certifi package 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

8. Pros & Cons of Bandit

โœ… Strengths

Feature Detail
Zero configuration Works instantly with pip 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 test assert 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=False in requests is a red flag - always use certifi or your system CA bundle
  • Never use /tmp as 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

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

Comments

No comments yet. Start the discussion.