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
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:
- Inventory your attack surface — list every external input: form fields, query parameters, JSON bodies, file uploads, headers, and cookies.
- 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.
- Implement layered defenses — use parameterized queries for injection, role checks for access control, and secure defaults for crypto and misconfigurations.
- Test with malicious inputs — send malformed data (SQL fragments, oversized payloads, unexpected types) to confirm your defenses actually block attacks.
- 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
LIKEandINclauses. - 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 setdebug=Falsein production and use environment variables. - Insecure deserialization with
pickle: Avoid loading untrusted data viapickle.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
pickleto load untrusted data — Python's pickle can execute arbitrary code during deserialization.
Variations
- Use Django's ORM instead of raw SQL — it automatically parameterizes queries and includes a CSRF framework.
- Adopt a security scanner like Bandit or Semgrep to catch common OWASP patterns in CI before deployment.
- Implement OWASP's validation and sanitization libraries (e.g.,
defusedxmlfor XML,bleachfor 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
picklewith 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.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.