Skip to main content

Python · SAST

Python SAST that runs entirely offline

Audit your Python code for OWASP Top 10 vulnerabilities, framework-specific misconfigurations and dependency risks — without sending a single line to the cloud. Covers Django, Flask, FastAPI, SQLAlchemy and the standard library.

Frameworks & libraries scanned

We test fixture coverage on these frameworks. New patterns are added with each builtin rule release.

Django Flask FastAPI SQLAlchemy Pyramid Tornado aiohttp Starlette Pandas NumPy Requests urllib3 PyYAML paramiko cryptography pyjwt

Top CWE classes detected

These CWE classes appear most often in modern codebases of this language. StaticCodeAudit ships dedicated rules for each, mapped to OWASP Top 10 / ISO 27001 / ASVS where relevant.

CWE-89

SQL Injection

String formatting or f-strings used to build SQL queries — typical with cursor.execute(f"SELECT ... {user_input}"), Django .raw() with concat, or SQLAlchemy .text() without bind parameters.

Detected by SCA rule: sql_injection_fstring

CWE-78

OS Command Injection

subprocess.run(shell=True, ...), os.system() with user input. The shell=True flag enables shell metacharacter abuse.

Detected by SCA rule: os_system_injection

CWE-502

Insecure Deserialization

pickle.loads(), yaml.load() without SafeLoader, shelve, or marshal on untrusted bytes. Equivalent to remote code execution.

Detected by SCA rule: unsafe_deserialization

CWE-918

SSRF

requests.get(user_url), urllib.request.urlopen() on user-controlled URLs without scheme/host allow-list. Lets attackers reach internal services (metadata endpoints, RDS, etc.).

Detected by SCA rule: taint_ssrf

CWE-798

Hardcoded Credentials

API keys, JWT secrets, database passwords in source files. Detected by entropy + known patterns (AWS, Stripe, GitHub, Slack, etc.).

Detected by SCA rule: hardcoded_secret

CWE-94

Code Injection via dynamic compile

Calls to Python\'s dynamic compilation built-ins on data partially controlled by user input.

Detected by SCA rule: dangerous_eval

Vulnerable vs. clean — three quick examples

CWE-89 Detected by SCA rule: sql_injection_fstring
❌ Vulnerable
# Vulnerable — Flask + SQLAlchemy raw
@app.route('/user/<name>')
def user(name):
    sql = f"SELECT * FROM users WHERE name = '{name}'"
    return db.engine.execute(sql).fetchone()
✅ Clean / safe
# Clean — bind parameter
@app.route('/user/<name>')
def user(name):
    sql = text("SELECT * FROM users WHERE name = :name")
    return db.engine.execute(sql, {"name": name}).fetchone()
CWE-78 Detected by SCA rule: os_system_injection
❌ Vulnerable
# Vulnerable — shell=True with user input
import subprocess
def ping(host):
    return subprocess.run(f"ping -c 1 {host}", shell=True)
✅ Clean / safe
# Clean — argument list, no shell
import subprocess
def ping(host):
    return subprocess.run(["ping", "-c", "1", host], check=False)
CWE-502 Detected by SCA rule: unsafe_deserialization
❌ Vulnerable
# Vulnerable — yaml.load without SafeLoader
import yaml
def load_config(blob):
    return yaml.load(blob)  # full Loader = RCE
✅ Clean / safe
# Clean — yaml.safe_load
import yaml
def load_config(blob):
    return yaml.safe_load(blob)

What StaticCodeAudit detects (this language)

  • SQL injection in raw SQL, f-strings, .format(), Django .raw(), SQLAlchemy .text() without binds
  • OS command injection (subprocess with shell=True, os.system, os.popen)
  • Insecure deserialization (pickle, yaml.load, marshal, shelve)
  • SSRF via requests, urllib, aiohttp with user-controlled URLs
  • Hardcoded secrets (AWS, Stripe, GitHub PAT, Slack tokens, JWT secrets, DB passwords, PEM keys)
  • XXE and XML billion-laughs (xml.etree, lxml without resolver disabled)
  • Path traversal (os.path.join with user input, open() without root anchor)
  • Insecure cookies (Flask/Django Secure / HttpOnly / SameSite flags missing)
  • CSRF exempt (@csrf_exempt, Flask without flask-wtf)
  • Insecure crypto (MD5/SHA1 for security, ECB mode, hardcoded IV, weak random)
  • Debug mode enabled in Flask / Django production config
  • Insecure JWT (algorithm none, hardcoded secret, no expiry)
  • Deprecated APIs (PyCrypto, ssl.TLSv1, urllib2-style)
  • PII in logs and exception messages
  • CI/CD misconfigurations (.gitlab-ci.yml, GitHub Actions: untrusted checkout, secrets in logs)

Curious how detection quality is actually measured? See the full cross-language benchmark methodology (OWASP Benchmark, NIST Juliet, precision/recall/F1 per language) →

OWASP Benchmark (Python) — independent validation

StaticCodeAudit was measured against OWASP Benchmark (Python), a public, reproducible test suite of 1,230 Python test cases — plus a real-world check against 3 frameworks reconstructed from published CVEs.

86.5%
F1 Score
96.0%
Recall
1,230
Test cases

CVE Test Suite, by framework — 2 of 3 at F1 = 100%

100%
Flask (CVE-reconstructed)
100%
Django (CVE-reconstructed)
80.0%
Python stdlib (CVE-reconstructed)

OWASP Benchmark Python scores 78.8% precision at 96.0% recall (1,230 cases, independent methodology) — real bugs get flagged, at the cost of some noise. The 3 framework rows above come from a separate, harder test: 45 cases reconstructed from real, historical CVEs across 8 frameworks and 5 languages. Python's 3 frameworks are its strongest showing in that suite; full breakdown (including where other languages score lower) on the <a href="/language-benchmarks.php" class="text-primary font-semibold hover:underline">language benchmarks page</a>.

Real vulnerabilities rarely live in one file

Flask and Django applications split logic across layers: a view function receives the request, a data-access function builds the query. A scanner limited to one file at a time never connects the two — it sees user input in views.py and a database call in db.py, but never the link between them. StaticCodeAudit follows the data across files.

views.py
@app.route("/orders")
def find_order():
    order_id = request.args.get("id")
    return jsonify(get_order(order_id))
db.py
def get_order(order_id):
    sql = f"SELECT * FROM orders WHERE id = {order_id}"
    return db.execute(sql).fetchone()

order_id comes from the HTTP request in the view, and reaches the SQL query one file later, unmodified — StaticCodeAudit reports it at the exact line where the query runs.

How does this compare to Bandit or Semgrep?

Bandit and Semgrep are the two tools that come up first when a Python team looks for a free security scanner. Both are legitimate, and many teams already run one of them — here's an honest comparison, not a takedown.

Bandit

Free, open-source

AST-based scanner built specifically for Python security issues (hardcoded passwords, shell=True, unsafe yaml.load). Mature and widely used, but single-file: no cross-file taint tracking, and findings aren't mapped to CWE/ISO 27001/ASVS out of the box.

Semgrep (Community)

Free, open-source core

Pattern-matching engine covering many languages, including Python, with a large open rule registry. The free tier covers most day-to-day rules; cross-file taint tracking, compliance reporting and some enterprise integrations sit behind Semgrep's paid tiers.

Full StaticCodeAudit vs Semgrep comparison →

Neither of these gets replaced by installing StaticCodeAudit — if Bandit already flags what your team needs, keep running it. What StaticCodeAudit adds on top: findings pre-mapped to CWE/OWASP/ISO 27001/ASVS/NIST CSF (a finding that's audit-ready, not just a rule ID), cross-file taint tracking across your whole Flask/Django app by default, and 7 other languages covered by the same binary — no separate scanner to configure per language. Many teams run both.

Audit your codebase

Audit your Python code for OWASP Top 10 vulnerabilities, framework-specific misconfigurations and dependency risks — without sending a single line to the cloud. Covers Django, Flask, FastAPI, SQLAlchemy and the standard library.

Download demo binary Book a walkthrough