Apply OWASP Top 10 to Python

Apply OWASP Top 10 to Python apps — hands-on Secure development tutorial. Identify common vulnerabilities in your Python code and fix them step by step.

Focus: apply owasp top 10 to python apps

Sponsored

Your Python app passes unit tests, deploys cleanly, and handles traffic — but a single SQL injection in an f-string or a missing SameSite cookie attribute can turn it into a headline security breach. The OWASP Top 10 isn't an abstract compliance checklist; it's a practical map of the most common and damaging vulnerabilities you'll face in real code. In this lesson, you'll stop treating security as an afterthought and start applying the OWASP Top 10 directly to your Python apps — from a raw SQL query to a Flask login form — with concrete fixes you can implement today.

The problem this lesson solves

Most Python developers learn security reactively: after a breach, after a penetration test, or after a code review flags a vulnerability. By then, fixes are expensive, urgent, and tangled in legacy code. The OWASP Top 10 gives you a proactive framework, but its generic descriptions ('Injection', 'Broken Access Control') don't tell you what that looks like in Python. You might know SQL injection is bad but still write:

query = f"SELECT * FROM users WHERE name = '{user_input}'"

because it's easy and you haven't seen the consequences. This lesson closes that gap, showing you exactly which Top 10 categories affect Python code and how to remediate them with standard libraries and modern practices.

Core concept / mental model

Think of the OWASP Top 10 as a security risk heat-map. Each category is a zone of danger, ranked by how often it's exploited and how much damage it causes. Your job is to walk through each zone and check whether your Python app has vulnerabilities.

Mental model: the attacker's perspective. For every input your app accepts (a form field, an API parameter, a JSON payload), ask: What happens if I send something unexpected? The Top 10 categories are essentially structured answers to that question — injection (can I alter your queries?), broken access control (can I access someone else's data?), cryptographic failures (can I decrypt your secrets?), and so on.

For Python specifically, three categories dominate: Injection, Broken Access Control, and Security Misconfiguration. Others like XSS and CSRF appear through the web frameworks you use (Flask, Django), while Deserialization and SSRF are trickier but just as real.

A useful technique is the OWASP Threat Model Checklist: for each endpoint, list the inputs, identify the Top 10 categories that apply, and verify or implement mitigations. We'll apply this in the hands-on section.

How it works step by step

The process of applying the OWASP Top 10 to a Python app follows a repeatable, five-step workflow:

  1. Inventory your attack surface — list every external input: form fields, query parameters, JSON bodies, file uploads, headers, and cookies.
  2. Map inputs to Top 10 categories — for each input, ask which vulnerability types could exploit it. A search parameter → Injection; a user ID in a URL → Broken Access Control; a redirect target → Open Redirect.
  3. Implement layered defenses — use parameterized queries for injection, role checks for access control, and secure defaults for crypto and misconfigurations.
  4. Test with malicious inputs — send malformed data (SQL fragments, oversized payloads, unexpected types) to confirm your defenses actually block attacks.
  5. Monitor and log — add security logging so future attempts are visible, and keep dependencies updated to patch known issues.

Each step is iterative; you revisit it as your app grows.

Hands-on walkthrough

Let's build a minimal Flask app that exercises several Top 10 categories and fix them one by one.

Setup

Ensure you have Python 3.10+ and Flask installed:

pip install flask

Example 1: Injection → parameterized queries

Start with a vulnerable login endpoint:

from flask import Flask, request, jsonify
import sqlite3

app = Flask(__name__)

def get_db():
    conn = sqlite3.connect('app.db')
    return conn

@app.route('/login')
def login():
    username = request.args.get('username')
    password = request.args.get('password')
    conn = get_db()
    cur = conn.execute(f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'")
    user = cur.fetchone()
    conn.close()
    if user:
        return jsonify({"status": "ok"})
    else:
        return jsonify({"status": "fail"}), 401

Attack: /login?username=admin'--&password=x bypasses authentication. Fix with a parameterized query:

from flask import Flask, request, jsonify
import sqlite3

app = Flask(__name__)

def get_db():
    return sqlite3.connect('app.db')

@app.route('/login')
def login():
    username = request.args.get('username')
    password = request.args.get('password')
    conn = get_db()
    cur = conn.execute(
        "SELECT * FROM users WHERE username = ? AND password = ?",
        (username, password)
    )
    user = cur.fetchone()
    conn.close()
    if user:
        return jsonify({"status": "ok"})
    else:
        return jsonify({"status": "fail"}), 401

The ? placeholders make the database treat inputs as data, not executable SQL. Run this and attempt the same attack — it will return 401.

Example 2: Broken Access Control

Add a profile endpoint that should be accessible only to the logged-in user:

@app.route('/profile/<int:user_id>')
def profile(user_id):
    # current_user is set from session in a real app
    current_user = get_current_user()  # placeholder
    if current_user.id != user_id:
        return jsonify({"error": "Forbidden"}), 403
    # fetch and return profile data

Always enforce authorization server-side. Never rely on hiding links or client-side checks.

Example 3: Cryptographic failures

Use only modern hashing. For passwords, use hashlib.scrypt or argon2 (Argon2 is the OWASP recommendation). Example with hashlib:

import hashlib
password = "supersecret"
salt = hashlib.sha256(os.urandom(16)).hexdigest()
hash = hashlib.scrypt(password.encode(), salt=salt.encode(), n=2**14, r=8, p=1)

Store salt and hash in your database, and always use HTTPS to protect data in transit.

Compare options / when to choose what

OWASP Category Python Mitigation Pros Cons
Injection Parameterized queries (sqlite3, SQLAlchemy) Simple, effective Requires discipline on all queries
Broken Access Control Role checks, decorators Precise control Must be applied everywhere
Cryptographic Failures cryptography lib, secrets Industry-standard Easy to misuse with weak algorithms
XSS Autoescaping in templates Built-in in Flask/Jinja Must avoid |safe filters
CSRF Flask-WTF CSRF tokens Simple Must add to every form
Security Misconfiguration Secure headers, debug off Low effort Forgotten in production

For most apps, parameterized queries are the right choice for injection; avoid raw SQL even for simple queries. For password hashing, prefer Argon2 over SHA-family if performance allows; otherwise, scrypt is a safe fallback.

Troubleshooting & edge cases

  • SQL injection still works despite parameterization: Ensure you're not concatenating the query string. Use bound parameters everywhere — including LIKE and IN clauses.
  • Access control bypass via object ID: If you use integers for IDs, attackers can enumerate them. Always check ownership before returning data.
  • Plaintext passwords in logs: Never log passwords. Use logging filters to redact sensitive fields.
  • Debug mode left on in production (app.run(debug=True)): This exposes stack traces and can allow remote code execution — always set debug=False in production and use environment variables.
  • Insecure deserialization with pickle: Avoid loading untrusted data via pickle.load(). Use JSON with schema validation instead.
  • CORS misconfiguration: Access-Control-Allow-Origin: * with credentials is a serious flaw — restrict origins.

What you learned & what's next

You now have a practical, repeatable method to apply the OWASP Top 10 to your Python apps. You can explain the core idea behind the framework, identify vulnerable patterns like unsafe SQL and missing auth, and complete a hands-on exercise that transforms insecure code into secure equivalents. You've also added a threat-model checklist to your development workflow.

Next, you'll move to Input Validation Posture in the Secure development track, where you'll learn to define strict validation rules for all external inputs — the first line of defense against injection, XSS, and other Top 10 categories. That lesson builds directly on the mindset shift you just made: expect malice in every input.

Practice recap

Take the vulnerable Flask login example and add a profile endpoint with proper authorization, plus a password hashing step using hashlib.scrypt. Test with a malicious SQL payload to confirm the fix. If you're ready, move to the next lesson on strict input validation.

Common mistakes

  • Using string formatting for SQL queries instead of parameterized queries — even when sanitizing with regex, it's still vulnerable.
  • Checking access control only on the UI (hiding buttons) and forgetting server-side enforcement — attackers can call endpoints directly.
  • Storing passwords with fast hashes like MD5 or SHA256 without salt — always use Argon2, scrypt, or bcrypt.
  • Leaving Flask debug mode on in production, which exposes stack traces and allows remote code execution.
  • Using pickle to load untrusted data — Python's pickle can execute arbitrary code during deserialization.

Variations

  1. Use Django's ORM instead of raw SQL — it automatically parameterizes queries and includes a CSRF framework.
  2. Adopt a security scanner like Bandit or Semgrep to catch common OWASP patterns in CI before deployment.
  3. Implement OWASP's validation and sanitization libraries (e.g., defusedxml for XML, bleach for HTML) to harden inputs.

Real-world use cases

  • A Flask e-commerce app protects user accounts from SQL injection by parameterizing all database queries and enforcing role-based access controls.
  • A Django REST API implements OWASP recommendations by hashing passwords with Argon2 and enabling CSRF protection to prevent account takeovers.
  • A Python microservice hardens its JSON endpoints against deserialization attacks by replacing pickle with validated JSON input.

Key takeaways

  • The OWASP Top 10 is a risk heat-map — map every external input to a category to find vulnerabilities.
  • Parameterized queries are the definitive fix for SQL injection in Python; never build SQL strings via f-strings.
  • Broken access control requires server-side authorization checks on every endpoint, not just UI hiding.
  • Cryptographic failures are avoided by using modern hashing (Argon2 or scrypt) and HTTPS.
  • Security misconfigurations like debug mode in production are low-effort but high-impact — always set secure defaults.
  • The threat-model checklist workflow (inventory → map → implement → test → monitor) makes OWASP practice repeatable.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.