
Why Every Developer Should Know the OWASP Top 10
The OWASP Top 10 is the most widely referenced standard for web application security risks. Published by the Open Web Application Security Project, it represents a broad consensus about the most critical security threats to web applications. If you build software that touches the internet, understanding these vulnerabilities isn't optional—it's essential.
The current list (2021, with 2025 update in progress) reflects a shift toward design flaws and architectural weaknesses rather than just implementation bugs.
The OWASP Top 10 (2021)
| # | Risk | Description | Moved From |
|---|---|---|---|
| A01 | Broken Access Control | Users act outside intended permissions | #5 → #1 |
| A02 | Cryptographic Failures | Sensitive data exposure through weak crypto | #3 → #2 |
| A03 | Injection | SQL, NoSQL, OS, LDAP injection | #1 → #3 |
| A04 | Insecure Design | Missing security architecture and design patterns | New |
| A05 | Security Misconfiguration | Default configs, open cloud storage, verbose errors | #6 → #5 |
| A06 | Vulnerable Components | Using components with known vulnerabilities | #9 → #6 |
| A07 | Auth Failures | Broken authentication and session management | #2 → #7 |
| A08 | Software & Data Integrity | Code and infrastructure without integrity verification | New |
| A09 | Security Logging Failures | Insufficient logging and monitoring | #10 → #9 |
| A10 | SSRF | Server-Side Request Forgery | New |
A01: Broken Access Control
The #1 risk. 94% of applications tested had some form of broken access control.
Vulnerable code:
# INSECURE: No authorization check
@app.route('/api/users/<user_id>/profile')
def get_profile(user_id):
# Any authenticated user can access ANY user's profile
user = User.query.get(user_id)
return jsonify(user.to_dict())
# INSECURE: Client-side role check only
@app.route('/api/admin/users')
def admin_users():
# Only checking role in frontend JavaScript, not backend
return jsonify([u.to_dict() for u in User.query.all()])Fixed code:
# SECURE: Proper authorization
@app.route('/api/users/<user_id>/profile')
@login_required
def get_profile(user_id):
# Users can only access their own profile
if str(current_user.id) != user_id and not current_user.is_admin:
abort(403)
user = User.query.get_or_404(user_id)
return jsonify(user.to_dict())
# SECURE: Server-side role verification
@app.route('/api/admin/users')
@login_required
@require_role('admin')
def admin_users():
return jsonify([u.to_dict() for u in User.query.all()])Common patterns:
- IDOR (Insecure Direct Object Reference): Changing
/api/orders/123to/api/orders/124 - Privilege escalation: Regular user accessing admin endpoints
- Missing function-level access control
- CORS misconfiguration allowing unauthorized origins
A02: Cryptographic Failures
Vulnerable patterns:
# INSECURE: MD5 for password hashing
import hashlib
password_hash = hashlib.md5(password.encode()).hexdigest()
# INSECURE: Hardcoded secret key
SECRET_KEY = "my-super-secret-key-123"
# INSECURE: HTTP for sensitive data
API_URL = "http://api.example.com/payments"Fixed code:
# SECURE: bcrypt with salt
from bcrypt import hashpw, gensalt, checkpw
password_hash = hashpw(password.encode(), gensalt(rounds=12))
# SECURE: Environment variable for secrets
import os
SECRET_KEY = os.environ["SECRET_KEY"]
# SECURE: HTTPS enforced
API_URL = "https://api.example.com/payments"A03: Injection
Injection dropped from #1 to #3, but remains dangerous. Modern frameworks help, but raw queries still appear in production code.
SQL Injection example:
# INSECURE: String concatenation in SQL
@app.route('/api/search')
def search():
query = request.args.get('q')
# Attacker sends: q='; DROP TABLE users; --
results = db.execute(
f"SELECT * FROM products WHERE name LIKE '%{query}%'"
)
return jsonify(results)
# SECURE: Parameterized query
@app.route('/api/search')
def search():
query = request.args.get('q', '')
results = db.execute(
"SELECT * FROM products WHERE name LIKE :query",
{"query": f"%{query}%"}
)
return jsonify(results)NoSQL Injection (MongoDB):
// INSECURE: Direct user input in query
app.post('/api/login', (req, res) => {
// Attacker sends: {"username": {"$gt": ""}, "password": {"$gt": ""}}
User.findOne({
username: req.body.username,
password: req.body.password
});
});
// SECURE: Input validation + sanitization
app.post('/api/login', (req, res) => {
const username = String(req.body.username);
const password = String(req.body.password);
User.findOne({ username })
.then(user => bcrypt.compare(password, user.passwordHash));
});A04: Insecure Design
New in 2021. This isn't about implementation bugs—it's about missing threat modeling and security patterns at the design phase.
Example: An e-commerce site allows unlimited password reset attempts with a 4-digit code. Even with rate limiting per IP, an attacker with a botnet can brute-force any account.
Secure design patterns:
- Threat modeling during architecture phase (STRIDE, PASTA)
- Secure design patterns library (reference architectures)
- Paved road: Make the secure way the easy way for developers
- Abuse case testing alongside functional testing
A05: Security Misconfiguration
Common Misconfigurations:
☐ Default credentials on admin panels (admin/admin)
☐ Unnecessary HTTP methods enabled (PUT, DELETE, TRACE)
☐ Directory listing enabled on web server
☐ Stack traces exposed in error responses
☐ Cloud storage buckets publicly accessible (S3, GCS)
☐ CORS set to Access-Control-Allow-Origin: *
☐ Debug mode enabled in production
☐ Default security headers missingSecurity headers every application should set:
# Nginx security headers
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header X-XSS-Protection "0" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'" always;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;A06: Vulnerable and Outdated Components
78% of codebases contain at least one vulnerability in third-party dependencies. Automated scanning is essential:
# npm audit — check Node.js dependencies
npm audit
npm audit fix
# pip-audit — check Python dependencies
pip install pip-audit
pip-audit
# Snyk — multi-language vulnerability scanner
snyk test
snyk monitor # continuous monitoring
# GitHub Dependabot — automatic PRs for vulnerable dependencies
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"A07-A10: Remaining Risks
A07 - Authentication Failures: Weak passwords, missing MFA, credential stuffing. Use established libraries (Passport.js, Django auth, NextAuth) rather than custom implementations.
A08 - Software & Data Integrity: Unverified CI/CD pipelines, unsigned updates, deserialization attacks. Always verify integrity of code and data through signatures and checksums.
A09 - Security Logging Failures: If you can't detect a breach, you can't respond. Log authentication events, access control failures, input validation failures, and application errors.
A10 - SSRF (Server-Side Request Forgery): The server makes requests on behalf of an attacker. Cloud metadata endpoints (169.254.169.254) are prime targets. Validate and sanitize all URLs, use allowlists.
Integrating OWASP into Your Development Process
Secure Development Lifecycle (SDL):
Requirements → Threat Modeling (A04: Insecure Design)
│
Design → Security Architecture Review
│
Development → SAST scanning, secure coding standards
│
Testing → DAST scanning, penetration testing
│
Deployment → Security hardening, dependency scanning
│
Operations → WAF, monitoring, incident response
│
Review → Lessons learned, update threat modelThe OWASP Top 10 isn't a checklist to complete—it's a framework for thinking about application security. Organizations that internalize these risks into their development culture build more resilient software.
Sources: OWASP Top 10 (2021), OWASP Testing Guide, OWASP Cheat Sheet Series


