Security

Cybersecurity Basics for Developers: OWASP Top 10 Explained (2026)

2026-07-05·13 min read
#security#OWASP#web security#developer

Security isn't your security team's job — it's everyone's job. Most data breaches exploit well-known vulnerabilities that developers could have prevented. The OWASP Top 10 lists the most critical web application security risks. Understanding them is the baseline for any developer building for the internet.

This guide explains each risk with real attack scenarios and practical fixes.

1. Broken Access Control

What it is: Users can access resources or perform actions they shouldn't be allowed to.

Attack Scenario

User A is logged in with ID 1001.
They change the URL to /api/users/1002/profile
And see User B's private data.

This is called IDOR (Insecure Direct Object Reference) and it's the #1 web vulnerability.

The Fix

# BAD: Only checks if user is logged in, not ownership
@app.get("/api/users/{user_id}/profile")
async def get_profile(user_id: int, current_user: User = Depends(get_current_user)):
    return await db.get_user(user_id)

# GOOD: Verifies the current user owns the resource
@app.get("/api/users/{user_id}/profile")
async def get_profile(user_id: int, current_user: User = Depends(get_current_user)):
    if current_user.id != user_id and not current_user.is_admin:
        raise HTTPException(403, "Not authorized")
    return await db.get_user(user_id)

Prevention Checklist

  • [ ] Verify ownership on every data access, not just authentication
  • [ ] Use UUIDs instead of sequential IDs (prevents enumeration)
  • [ ] Implement role-based access control (RBAC)
  • [ ] Deny by default; require explicit permission
  • [ ] Log access control failures

2. Cryptographic Failures

What it is: Sensitive data exposed due to weak or missing encryption.

Common Mistakes

// BAD: Storing passwords in plain text
db.query('INSERT INTO users (email, password) VALUES ($1, $2)', [email, password]);

// BAD: Using MD5 or SHA1 (broken algorithms)
const hash = crypto.createHash('md5').update(password).digest('hex');

// GOOD: Using bcrypt with proper salt rounds
import bcrypt from 'bcrypt';
const hashedPassword = await bcrypt.hash(password, 12);

Data in Transit

# Force HTTPS with HSTS
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

# Modern TLS only
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;

Data at Rest

  • Database fields: Encrypt SSNs, credit card numbers, health data
  • Backups: Encrypt backup volumes
  • Logs: Never log passwords, tokens, or PII
# BAD: Logging sensitive data
logger.info(f"User login: email={user.email}, password={password}")

# GOOD: Logging without sensitive data
logger.info(f"User login: user_id={user.id}, ip={request.client.host}")

3. Injection

What it is: Untrusted data is sent to an interpreter as part of a command or query.

SQL Injection

# BAD: String concatenation allows SQL injection
query = f"SELECT * FROM users WHERE email = '{email}'"
db.execute(query)

# If email = "admin@x.com' OR '1'='1" → returns ALL users

# GOOD: Parameterized queries (impossible to inject)
query = "SELECT * FROM users WHERE email = ?"
db.execute(query, (email,))

Command Injection

# BAD: User input directly in shell command
import subprocess
subprocess.call(f"ping {user_input}", shell=True)

# If user_input = "google.com; rm -rf /" → disaster

# GOOD: Use argument list, never shell=True
subprocess.call(["ping", user_input])

Prevention Checklist

  • [ ] Use parameterized queries (every time, no exceptions)
  • [ ] Use ORMs (they parameterize automatically)
  • [ ] Validate and sanitize input
  • [ ] Use allowlists, not blocklists, for input validation
  • [ ] Escape output when rendering in HTML (prevents XSS)

4. Insecure Design

What it is: The application architecture itself has security flaws that can't be fixed by code alone.

Examples

  • No rate limiting on login (allows brute force)
  • Password reset via security questions with guessable answers
  • Admin functions accessible through regular UI without separation
  • No transaction limits (allows unlimited purchases/transfers)

Fixes

# Rate limiting on authentication endpoints
from slowapi import Limiter
limiter = Limiter(key_func=get_remote_address)

@app.post("/login")
@limiter.limit("5/minute")  # Max 5 login attempts per minute
async def login(request: Request, credentials: LoginRequest):
    ...
# Account lockout after failed attempts
if user.failed_login_count >= 5:
    user.locked_until = datetime.utcnow() + timedelta(minutes=30)
    db.commit()
    raise HTTPException(423, "Account locked. Try again in 30 minutes.")

5. Security Misconfiguration

What it is: The application, server, or framework is configured insecurely.

Common Issues

# BAD: Debug mode in production
app = Flask(debug=True)  # Exposes stack traces and interactive debugger

# GOOD: Environment-based configuration
app = Flask(debug=os.getenv("FLASK_ENV") == "development")
# BAD: Default credentials
# Database password is "password" or "admin"

# GOOD: Strong, unique passwords
POSTGRES_PASSWORD=$(openssl rand -base64 32)

Header Configuration

# Essential security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'" always;
add_header Strict-Transport-Security "max-age=63072000" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;

Prevention Checklist

  • [ ] Disable debug mode in production
  • [ ] Remove default accounts and change default passwords
  • [ ] Disable directory listing
  • [ ] Set security headers (use securityheaders.com)
  • [ ] Keep all dependencies updated
  • [ ] Remove unused features and endpoints

6. Vulnerable and Outdated Components

What it is: Using libraries or frameworks with known security vulnerabilities.

The Problem

// package.json — you might have vulnerable dependencies
{
  "dependencies": {
    "lodash": "4.17.20",  // Vulnerable to prototype pollution
    "express": "4.17.0"   // Multiple CVEs
  }
}

The Fix

# Node.js/npm
npm audit
npm audit fix
npm audit fix --force  # Major upgrades (test before deploying!)

# Python/pip
pip install pip-audit
pip-audit

# Go
govulncheck ./...

# Ruby
bundle audit

# General: Use Dependabot (GitHub) or Renovate

Prevention Checklist

  • [ ] Enable automated dependency scanning (Dependabot, Snyk)
  • [ ] Regularly update dependencies
  • [ ] Remove unused dependencies
  • [ ] Subscribe to security advisories for critical libraries
  • [ ] Pin versions in production (no floating ranges)

7. Identification and Authentication Failures

What it is: Weak authentication allows attackers to compromise user accounts.

Password Requirements

# BAD: Weak password policy
if len(password) >= 6:
    return True

# GOOD: Strong password policy using zxcvbn
from zxcvbn import zxcvbn

def validate_password(password: str) -> tuple[bool, str]:
    if len(password) < 12:
        return False, "Password must be at least 12 characters"

    result = zxcvbn(password)
    if result['score'] < 3:
        return False, "Password is too weak. Use a mix of characters, numbers, and symbols."

    return True, "Password is strong"

Session Management

# JWT best practices
TOKEN_CONFIG = {
    "algorithm": "HS256",  # or RS256 for distributed systems
    "expiry_minutes": 15,  # Short-lived access tokens
    "refresh_expiry_days": 7,  # Longer refresh tokens
}

# Rotate tokens on privilege changes (login, privilege escalation)
# Invalidate all sessions on password change

MFA (Multi-Factor Authentication)

# Use TOTP (Time-based One-Time Password)
import pyotp

# Generate secret for user
secret = pyotp.random_base32()

# Verify code
totp = pyotp.TOTP(secret)
if totp.verify(user_provided_code, valid_window=1):
    # Valid MFA code

8. Software and Data Integrity Failures

What it is: Code or data that has been tampered with, often through supply chain attacks.

Examples

  • Installing packages from untrusted sources
  • Unsigned software updates
  • CI/CD pipelines without proper access controls

Fixes

# Lock files prevent dependency tampering
# package-lock.json (Node.js)
# poetry.lock (Python)
# go.sum (Go)

# Verify package signatures where available
npm audit signatures
pip install --require-hashes -r requirements.txt
# GitHub Actions: Pin actions by SHA, not by tag
- uses: actions/checkout@4c2b5d1   # Pinned (safe)
  # NOT: actions/checkout@v4        # Mutable (risky)

9. Security Logging and Monitoring Failures

What it is: Security incidents happen but nobody notices because there's no logging or alerting.

What to Log

import logging
import structlog

logger = structlog.get_logger()

# Authentication events
logger.info("login_success", user_id=user.id, ip=request.client.host)
logger.warning("login_failed", email=email, ip=request.client.host, reason="bad_password")

# Authorization events
logger.warning("access_denied", user_id=user.id, resource=f"/api/users/{user_id}", ip=request.client.host)

# Data access events
logger.info("data_export", user_id=user.id, records=500, resource="user_list")

# System events
logger.error("rate_limit_exceeded", ip=request.client.host, endpoint=request.url.path)

Alerting Setup

# Alert on these patterns:
# - 5+ failed logins from same IP in 1 minute → brute force
# - Login from new geographic location → possible account takeover
# - Mass data export → possible data exfiltration
# - Access control failures → probing attack
# - Error rate spike → possible attack or outage

10. Server-Side Request Forgery (SSRF)

What it is: The server can be tricked into making requests to internal resources.

Attack Scenario

App has a feature: "Fetch URL and display content"
User submits: http://169.254.169.254/latest/meta-data/
Server fetches AWS metadata → returns cloud credentials to attacker

The Fix

import ipaddress
import socket
from urllib.parse import urlparse

ALLOWED_SCHEMES = ['http', 'https']

def validate_url(url: str) -> bool:
    parsed = urlparse(url)

    # Only allow HTTP(S)
    if parsed.scheme not in ALLOWED_SCHEMES:
        return False

    # Resolve hostname and check it's not internal
    try:
        ip = socket.gethostbyname(parsed.hostname)
        ip_obj = ipaddress.ip_address(ip)

        # Block private/internal IPs
        if ip_obj.is_private or ip_obj.is_loopback or ip_obj.is_link_local:
            return False
        if ip_obj in ipaddress.ip_network('169.254.0.0/16'):  # Cloud metadata
            return False
    except socket.gaierror:
        return False

    return True

# Use this validation before any server-side URL fetch

Quick Security Audit Checklist

Run this on your application today:

  • [ ] All database queries use parameterized statements
  • [ ] Passwords are hashed with bcrypt or argon2
  • [ ] HTTPS is enforced (HSTS header)
  • [ ] Security headers are set (CSP, X-Frame-Options, X-Content-Type-Options)
  • [ ] Rate limiting on authentication endpoints
  • [ ] Session tokens expire and rotate
  • [ ] Error messages don't leak stack traces in production
  • [ ] Dependencies are scanned for known vulnerabilities
  • [ ] User file uploads are validated and sandboxed
  • [ ] CORS is restrictive (not *)
  • [ ] Sensitive data in environment variables, not code
  • [ ] Logs capture security events without exposing PII
  • [ ] Admin interfaces require separate authentication

Tools Every Developer Should Use

| Tool | Purpose | Cost | |------|---------|------| | OWASP ZAP | Web vulnerability scanner | Free | | Snyk | Dependency scanning | Free tier | | GitGuardian | Secret detection in code | Free tier | | securityheaders.com | Check HTTP security headers | Free | | ssllabs.com | SSL/TLS configuration check | Free | | Burp Suite Community | Manual security testing | Free | | TruffleHog | Find secrets in Git history | Free |

Conclusion

Security isn't something you bolt on after building features. It's a mindset you apply throughout development. The OWASP Top 10 isn't an exhaustive list — it's the minimum baseline.

The most important habits:

  1. Never trust user input. Validate, sanitize, and parameterize everything.
  2. Never store secrets in code. Use environment variables and secret managers.
  3. Always verify authorization. Authentication ≠ authorization.
  4. Keep dependencies updated. Vulnerabilities are found and fixed constantly.
  5. Log security events. You can't respond to incidents you can't detect.

Security is an ongoing process, not a one-time checklist. But if every developer followed just the items on this list, 90% of web vulnerabilities would disappear.