Web Application Security: OWASP Top 10 and Beyond
Web application security is non-negotiable in 2025. This guide covers the OWASP Top 10 vulnerabilities and practical defenses for modern web applications.
OWASP Top 10 Overview
The OWASP Top 10 represents the most critical web application security risks:
- Broken Access Control
- Cryptographic Failures
- Injection
- Insecure Design
- Security Misconfiguration
- Vulnerable and Outdated Components
- Identification and Authentication Failures
- Software and Data Integrity Failures
- Security Logging and Monitoring Failures
- Server-Side Request Forgery (SSRF)
Let’s dive into each with practical defenses.
1. Broken Access Control
Access control enforces policy such that users cannot act outside their intended permissions.
Vulnerabilities
- Bypassing access control checks by modifying URL, HTML, or API requests
- Elevation of privilege (acting as admin without being logged in)
- Metadata manipulation (JWT token tampering)
- CORS misconfiguration allowing unauthorized API access
Defenses
# Never rely on client-side checks
def get_user_profile(user_id):
# BAD: Trusting client-supplied user_id
# user = User.get(request.args.get('user_id'))
# GOOD: Use authenticated session
current_user = get_current_user()
# Verify access
if current_user.id != user_id and not current_user.is_admin:
raise Forbidden()
return User.get(user_id)
Implement deny-by-default:
# Explicit permission checks
@require_permission('users:read')
def list_users():
return User.query.all()
@require_permission('users:write')
def create_user(data):
# ...
2. Cryptographic Failures
Protect data in transit and at rest.
Vulnerabilities
- Transmitting data in clear text (HTTP, FTP, SMTP)
- Using old or weak cryptographic algorithms
- Default or weak encryption keys
- Improper certificate validation
Defenses
# Use strong encryption
from cryptography.fernet import Fernet
# Generate a key once, store securely
key = Fernet.generate_key()
cipher = Fernet(key)
# Encrypt sensitive data
encrypted = cipher.encrypt(b"sensitive data")
# Decrypt
decrypted = cipher.decrypt(encrypted)
Use TLS 1.3 for all connections:
# Nginx configuration
ssl_protocols TLSv1.3;
ssl_prefer_server_ciphers off;
ssl_ciphers 'TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256';
3. Injection
Injection flaws occur when untrusted data is sent to an interpreter.
SQL Injection
# BAD: String concatenation
query = f"SELECT * FROM users WHERE id = {user_id}"
# GOOD: Parameterized queries
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
Command Injection
# BAD
os.system(f"ping {user_input}")
# GOOD: Use subprocess with shell=False
import subprocess
subprocess.run(["ping", "-c", "1", user_input], check=True)
XSS Prevention
# Escape output
from markupsafe import escape
def render_comment(comment):
return f"<div>{escape(comment)}</div>"
# Or use a template engine that auto-escapes
4. Insecure Design
Security must be designed in, not bolted on.
Threat Modeling
For every feature, ask:
- What are we building?
- What can go wrong?
- What are we doing about it?
- Did we do a good job?
Secure Design Patterns
# Use allow-lists, not deny-lists
ALLOWED_FILE_TYPES = {'.jpg', '.png', '.gif'}
def validate_upload(filename):
ext = os.path.splitext(filename)[1].lower()
if ext not in ALLOWED_FILE_TYPES:
raise ValueError("File type not allowed")
5. Security Misconfiguration
Common Issues
- Default accounts and passwords
- Unnecessary features enabled
- Error messages revealing stack traces
- Outdated software
Defenses
# Never expose stack traces in production
app = Flask(__name__)
app.config['DEBUG'] = False
app.config['PROPAGATE_EXCEPTIONS'] = False
# Custom error handlers
@app.errorhandler(500)
def internal_error(error):
return jsonify({'error': 'Internal server error'}), 500
Security headers:
from flask_talisman import Talisman
talisman = Talisman(
app,
force_https=True,
strict_transport_security=True,
content_security_policy={
'default-src': "'self'",
'script-src': "'self'",
'style-src': "'self'",
}
)
6. Vulnerable and Outdated Components
Dependency Management
# Regularly update dependencies
pip list --outdated
pip install -U package_name
# Use tools like safety
pip install safety
safety check
Software Composition Analysis
Use tools like Snyk, Dependabot, or OWASP Dependency-Check:
# GitHub Dependabot
version: 2
updates:
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"
7. Identification and Authentication Failures
Multi-Factor Authentication
import pyotp
# Generate secret
secret = pyotp.random_base32()
# Verify TOTP
totp = pyotp.TOTP(secret)
is_valid = totp.verify(user_provided_code)
Password Security
import bcrypt
# Hash passwords
password = b"user_password"
hashed = bcrypt.hashpw(password, bcrypt.gensalt(rounds=12))
# Verify
bcrypt.checkpw(password, hashed)
8. Software and Data Integrity Failures
Dependency Confusion
Verify package integrity:
# requirements.txt with hashes
requests==2.31.0 --hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1
CI/CD Security
Sign your artifacts:
# Sign Docker images
docker trust sign myimage:latest
# Verify signatures
cosign verify myimage:latest
9. Security Logging and Monitoring Failures
Comprehensive Logging
import structlog
logger = structlog.get_logger()
# Log security events
logger.info("login_attempt",
user_id=user.id,
ip_address=request.remote_addr,
success=False,
reason="invalid_password"
)
Alerting
Set up alerts for:
- Multiple failed login attempts
- Privilege escalation attempts
- Unusual data access patterns
- Configuration changes
10. Server-Side Request Forgery (SSRF)
Defenses
import ipaddress
from urllib.parse import urlparse
def is_safe_url(url):
"""Prevent SSRF to internal IPs."""
parsed = urlparse(url)
if parsed.scheme not in ('http', 'https'):
return False
try:
ip = ipaddress.ip_address(parsed.hostname)
# Block private IPs
if ip.is_private or ip.is_loopback:
return False
except ValueError:
pass # hostname, not IP
return True
Defense in Depth
No single control is sufficient. Layer your defenses:
- Perimeter: WAF, DDoS protection
- Application: Input validation, output encoding
- Authentication: MFA, session management
- Authorization: Least privilege, deny by default
- Data: Encryption at rest and in transit
- Monitoring: Logging, alerting, incident response
Building a Security Review Routine
Knowledge of the Top 10 is useless without a process that applies it. Bake a lightweight security review into your development rhythm rather than treating security as a pre-launch audit.
For every pull request, ask three questions: does this handle untrusted input, does it change an authorization decision, and does it touch cryptographic or authentication code? A “yes” to any of them triggers a closer look. This triage takes thirty seconds and catches the majority of issues before they merge.
Supplement manual review with automated scanning in CI — dependency checks on every build, static analysis on every commit, and a dynamic scan against staging weekly. Tools find the mechanical issues; humans find the design flaws. Neither alone is sufficient, which is why mature teams also run periodic penetration tests and bug bounty programs — see our bug bounty guide for how the offensive side approaches the same applications you’re defending.
Security Headers Quick Reference
A few HTTP headers shut down entire attack classes with almost no effort. Set these at your reverse proxy or framework level:
Content-Security-Policy: default-src 'self'
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: geolocation=(), camera=()
The CSP is the heavyweight — it tells the browser exactly which sources may load scripts, styles, and frames, neutralizing most XSS even when an injection slips through. Start with a report-only policy (Content-Security-Policy-Report-Only) to see what would break, tighten it over a week, then enforce. X-Content-Type-Options: nosniff stops MIME confusion, and X-Frame-Options blocks clickjacking. These five lines cost nothing and remove whole categories of findings from your next pentest report.
Conclusion
Web application security requires constant vigilance. The OWASP Top 10 provides a framework, but security is an ongoing process, not a destination.
Start with the fundamentals: input validation, output encoding, and proper authentication. Build security into your development lifecycle, not as an afterthought. Regular testing, code review, and staying current with threats are essential.
The cost of prevention is always less than the cost of a breach.