OWASP Top 10
Table of Contents
Why the OWASP Top 10 Matters
As a backend developer, you do not have to memorize every security attack that exists, but you must be familiar with the most common and dangerous ones. The OWASP Top 10 is a community maintained list of the most critical web application security risks.
Think of it as a “priority list” of things you must defend against first. Many real world breaches happen because of these exact issues, often in very simple ways.
In this chapter, you will learn what each OWASP Top 10 category means in practical backend terms, what a vulnerable backend might look like, and what a safer version looks like.
Overview of the OWASP Top 10
OWASP periodically updates the Top 10. The exact names and order can change, but the themes are stable. A practical way to think of them for backend work is:
| Category (simplified) | Typical backend mistake |
|---|---|
| Broken access control | Not checking “who can do what” on every request |
| Cryptographic failures | Storing or sending sensitive data without proper protection |
| Injection | Putting untrusted data directly into SQL, commands, etc. |
| Insecure design | Designing a feature in a way that is impossible to secure |
| Security misconfiguration | Bad or default server / framework settings |
| Vulnerable & outdated components | Using old libraries with known vulnerabilities |
| Identification & authentication failures | Weak login, broken session, flawed password handling |
| Software & data integrity failures | No integrity checks for code, configs, or data |
| Security logging & monitoring failures | No useful logs, so you never see attacks or cannot respond |
| Server-side request forgery (SSRF) | Backend can be tricked to make dangerous HTTP requests |
The rest of this chapter walks through these 10 from a backend perspective with examples.
A01: Broken Access Control
Access control is about “who can do what to which resource.” Broken access control happens when the backend does not correctly enforce this.
Common backend patterns that cause it:
- Relying on the frontend to hide admin features.
- Using only “is logged in” checks, and not checking ownership or roles.
- Using predictable IDs or object references without checking access.
Example: Missing authorization check
Imagine an API endpoint:
# /orders/{order_id}
@app.get("/orders/{order_id}")
def get_order(order_id: int, db: Session = Depends(get_db)):
return db.query(Order).filter(Order.id == order_id).first()If you do not check that the current user owns the order, any logged in user can fetch any order by trying different IDs.
A safer version:
@app.get("/orders/{order_id}")
def get_order(
order_id: int,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
order = (
db.query(Order)
.filter(Order.id == order_id, Order.user_id == current_user.id)
.first()
)
if not order:
raise HTTPException(status_code=404, detail="Order not found")
return orderImportant patterns:
- Always check ownership on resources like orders, profiles, files.
- Enforce role based access control (RBAC) or permissions on every sensitive operation.
- Use server side checks only. Never trust what the frontend hides or shows.
A02: Cryptographic Failures
Cryptographic failures usually mean sensitive data is not protected correctly. This is often more about configuration or misuse than about writing your own cryptography code.
Typical backend mistakes:
- Storing passwords in plain text.
- Using HTTP instead of HTTPS for login or API calls.
- Using weak or homegrown encryption.
- Exposing secrets in logs or responses.
Example: Bad password storage
Bad:
# Storing raw password
user.password = form.passwordBetter:
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def hash_password(plain_password: str) -> str:
return pwd_context.hash(plain_password)
user.password_hash = hash_password(form.password)Example: Sending sensitive data over HTTP
If an API that handles personal data is accessible over http://example.com/api, then anyone on the network can eavesdrop. You must:
- Use HTTPS in production.
- Set secure cookies only over HTTPS.
- Avoid logging secrets, tokens, or full credit card numbers.
Never store passwords in plain text or with a simple hash like MD5 or SHA1. Always use a password hashing algorithm designed for this purpose, for example bcrypt, Argon2, or PBKDF2.
You will work with HTTPS and TLS in more depth in the security chapters about HTTPS and TLS.
A03: Injection
Injection occurs when untrusted input is interpreted as code or a command. For backend developers, the most common form is SQL Injection.
Other types include:
- Command injection (shell commands).
- LDAP, NoSQL, or template injection.
SQL Injection example
Bad Python code:
def get_user(db, username: str):
# Very unsafe
query = f"SELECT * FROM users WHERE username = '{username}'"
return db.execute(query).fetchone()
If the user sends username = "admin' OR '1'='1", the query becomes:
SELECT * FROM users WHERE username = 'admin' OR '1'='1'which matches all users.
Safe pattern, with parameterized queries:
def get_user(db, username: str):
return db.execute(
"SELECT * FROM users WHERE username = :username",
{"username": username},
).fetchone()Or with SQLAlchemy ORM:
db.query(User).filter(User.username == username).first()Always use parameterized queries or ORM query builders. Never build SQL by concatenating user input.
This idea also applies to other interpreters. Avoid building shell commands like:
os.system("rm " + user_input)A04: Insecure Design
Insecure design means the problem is in the concept of how the feature works, not only in the code.
Examples of insecure designs:
- “Password reset” that only asks for a username and then shows the current password.
- “Secret links” that use easily guessable IDs without expiration.
- Relying only on client side checks for critical rules, for example price validation.
Example: Insecure password reset flow
Insecure design:
- User submits email.
- Backend shows security questions, then directly changes password.
If an attacker can guess the answers or intercept the flow, they own the account.
More secure design:
- User submits email.
- Backend generates a random, one time token and sends a link by email.
- The link has a short expiration, is tied to that account, and can be used only once.
- User sets a new password via that link.
- Token is invalidated after use.
Even if the code is written correctly, a design that reveals full passwords or uses permanent, guessable reset links will never be secure.
A05: Security Misconfiguration
Security misconfiguration is about bad or missing security settings. This includes:
- Leaving default admin credentials.
- Exposing debug or stack traces in production.
- Overly permissive CORS configuration.
- Running database or admin panels directly exposed to the internet without protection.
Example: Debug mode in production
In some Python frameworks:
app.run(debug=True)In production, this can show internal variables, environment values, and more if an error occurs. Attackers can use this information.
Safer pattern:
- Turn off debug in production.
- Use custom error pages and structured error logging.
Example: Overly permissive CORS
CORS is covered in another chapter. A common misconfiguration is:
origins = ["*"] # for all methods including credentials
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True, # with "*"
allow_methods=["*"],
allow_headers=["*"],
)If combined with cookies or auth headers, this can expose user data to any site.
Better is to set specific trusted origins:
origins = ["https://example.com", "https://admin.example.com"]Never leave default passwords, public debug modes, or open admin panels in production. Always review security related configuration before deployment.
A06: Vulnerable and Outdated Components
Modern backends depend on many third party libraries, frameworks, and services. If you use old versions with known vulnerabilities, your application can be compromised even if your own code is correct.
Common issues:
- Not updating dependencies, even after security releases.
- Not pinning versions, which makes behavior unpredictable.
- Ignoring vulnerability alerts from your package manager or repository hosting.
Example: Unmanaged dependencies
requirements.txt:
fastapi
uvicorn
sqlalchemyThis means “latest available version at install time,” which can change across deployments.
Better:
fastapi==0.115.0
uvicorn[standard]==0.30.0
sqlalchemy==2.0.35Then regularly:
- Update versions.
- Run security scanners (for example
pip-audit, GitHub Dependabot). - Read release notes of critical components like your framework or ORM.
Patterns for safety:
- Pin versions in production.
- Have a regular update routine.
- Use tools that report known vulnerabilities.
A07: Identification and Authentication Failures
This category concerns login, session management, and related logic. Common failures:
- Not validating passwords properly.
- Using predictable or insecure session IDs.
- Not expiring sessions or tokens.
- Allowing brute force login attempts without rate limiting.
- Not verifying email for critical accounts if required by your threat model.
Example: Weak login implementation
Bad:
def login(username, password):
user = get_user_by_username(username)
if not user:
raise HTTPException(401, "Invalid credentials")
# raw password comparison
if user.password != password:
raise HTTPException(401, "Invalid credentials")Better:
def verify_password(plain_password, password_hash):
return pwd_context.verify(plain_password, password_hash)
def login(username, password):
user = get_user_by_username(username)
if not user or not verify_password(password, user.password_hash):
# do not reveal if username or password was wrong
raise HTTPException(401, "Invalid credentials")Also consider:
- Account lockout or throttling after several failed attempts.
- Secure session cookies with
HttpOnly,Secure, andSameSitesettings. - Token expiration and refresh strategies when using JWT.
You will learn more about these in the authentication and authorization sections of the course.
Never store or compare raw passwords. Use proper password hashing and session or token security patterns.
A08: Software and Data Integrity Failures
This category covers situations where the application or infrastructure does not protect against:
- Tampered updates or dependencies.
- Malicious changes to configuration or data.
- Injection into build pipelines or deployment scripts.
Typical backend related problems:
- Downloading and executing external code at runtime without verification.
- Relying on unsigned updates.
- Letting clients decide trust boundaries, for example allowing the client to send roles or permissions that are then trusted.
Example: Trusting client side roles
Bad:
# Frontend sends role in JSON:
# { "username": "alice", "role": "admin" }
def create_user(data):
user.role = data["role"] # trust client!
...
Anyone who intercepts the request can change "role": "admin".
Safer:
- The backend determines roles based on business rules, not from arbitrary client input.
- Changes to roles or permissions require admin authenticated operations.
For code and configuration integrity:
- Use locked dependency files.
- Use signed images or artifacts if your infrastructure supports it.
- Restrict who can change deployment pipelines and secrets.
A09: Security Logging and Monitoring Failures
If you do not log important events or do not monitor those logs, you will not know that an attack is happening or has succeeded.
Common issues:
- No logs for authentication failures or permission denied events.
- Logs missing timestamps, request identifiers, or user identifiers.
- Logging sensitive data like full tokens or passwords.
- No alerts for suspicious patterns.
Example: Useful logging for a failed login
Instead of:
logger.info("Login failed")Better:
logger.warning(
"Login failed",
extra={"username": username, "ip": client_ip, "reason": "invalid_credentials"},
)Be careful not to log the password. You might log an anonymized user identifier, but not secrets.
Patterns:
- Log:
- Login failures and successes.
- Access denied events.
- Exceptions in API endpoints.
- Include:
- Timestamp.
- User ID or subject (if known).
- Request ID or correlation ID.
- Monitor:
- Use tools to aggregate logs and define alerts.
Never log raw passwords, secret keys, or full payment data. Logs must be useful for detection but must not leak sensitive information.
A10: Server-Side Request Forgery (SSRF)
SSRF happens when your backend makes HTTP requests to URLs that the client controls, and you do not restrict where it can connect.
Attackers can use this to:
- Access internal services on private networks.
- Call cloud metadata services (often at
http://169.254.169.254) to steal credentials. - Reach internal admin panels that are not exposed publicly.
Example: Unrestricted URL fetcher
Bad:
import requests
@app.post("/fetch")
def fetch_url(body: dict):
url = body["url"]
resp = requests.get(url)
return {"status": resp.status_code, "body": resp.text}
If the attacker sends {"url": "http://127.0.0.1:8000/admin"}, your server accesses localhost, which might be an internal admin interface.
Mitigations:
- Maintain an allowlist of domains that can be called.
- Reject requests to:
- Private IP ranges like
10.0.0.0/8,192.168.0.0/16,127.0.0.0/8. - Links without schemes or with unusual schemes.
- Use network controls when possible, for example firewall rules.
Example check (simplified, do not use as is in production):
from urllib.parse import urlparse
import ipaddress
import socket
def is_private_host(url: str) -> bool:
parsed = urlparse(url)
hostname = parsed.hostname
if not hostname:
return True
ip = socket.gethostbyname(hostname)
ip_obj = ipaddress.ip_address(ip)
return ip_obj.is_private or ip_obj.is_loopbackThen:
if is_private_host(url):
raise HTTPException(status_code=400, detail="URL not allowed")Putting the OWASP Top 10 into Practice
You do not need to implement everything perfectly on day one, but you should:
- Recognize each category: When you design or review code, ask “Does this touch authentication, access control, injection, or logging?”
- Adopt safe defaults:
- Use parameterized SQL and ORMs.
- Use HTTPS in production.
- Hash passwords with a proper algorithm.
- Check access on every protected resource.
- Integrate checks into your workflow:
- Code review with security in mind.
- Use linters and security tools where possible.
- Keep dependencies updated and monitored.
Most serious security incidents are caused by basic mistakes that the OWASP Top 10 describes. If you build the habit of considering these 10 areas when you design and code backend features, you will avoid many common and dangerous vulnerabilities.
In later chapters, you will dive deeper into specific topics like SQL Injection, XSS, CSRF, HTTPS, CORS, authentication, and secure configuration, where many of these OWASP Top 10 ideas appear again in more detail.
Views: 7
KAHIBARO