DEV Community

Cover image for Why Every Developer Should Understand the OWASP Top 10 Before Writing Code
KS Softech Private Limited
KS Softech Private Limited

Posted on

Why Every Developer Should Understand the OWASP Top 10 Before Writing Code

Most developers encounter security the hard way: a breach report, a penetration test that returns a wall of red, or a frantic Slack message at 2 a.m. after a production incident. The irony is that the majority of vulnerabilities exploited in the wild today aren't novel zero-days — they're the same categories of weaknesses that have existed for over two decades, meticulously documented by OWASP in their Top 10 list.

If you're building web applications and you haven't internalized the OWASP Top 10, you're not writing secure code. You might be writing working code, but that's a different thing entirely.

This article breaks down each category with technical depth, explains why it matters at the code level, and gives you practical patterns to eliminate these vulnerabilities before they ship.

What Is the OWASP Top 10?

The Open Web Application Security Project (OWASP) is a nonprofit foundation that produces open-source security research, tools, and documentation. Their Top 10 list — most recently updated in 2021 — represents a broad consensus about the most critical security risks to web applications, ranked by prevalence, detectability, and impact.

The 2021 list is:

  • 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)

Understanding these isn't optional for developers building production systems. Let's go deep on each one.

1. Broken Access Control

Rank: 1 (moved up from 5)

Broken Access Control is now the most prevalent vulnerability category, found in 94% of tested applications. It occurs when users can act outside their intended permissions reading other users' data, modifying records they don't own, or accessing admin functionality without authorization.

The Technical Reality

Consider a REST API endpoint like:

GET /api/users/1234/profile

A naive implementation checks if the requester is authenticated but not who they are relative to the resource:

javascript// ❌ Vulnerable: only checks authentication, not authorization
app.get('/api/users/:id/profile', authenticate, async (req, res) => {
  const user = await db.users.findById(req.params.id);
  res.json(user);
});
Enter fullscreen mode Exit fullscreen mode

An authenticated user with ID 5678 can simply change the id parameter to 1234 and read another user's profile. This is an Insecure Direct Object Reference (IDOR) — one of the most common and damaging sub-categories.

The Fix

Enforce ownership at the query level, not just in conditional logic:

javascript// ✅ Safe: the query itself enforces ownership
app.get('/api/users/:id/profile', authenticate, async (req, res) => {
  // Only return data if the requesting user owns this record
  const user = await db.users.findOne({
    id: req.params.id,
    ownerId: req.user.id  // Bound to the authenticated user
  });

  if (!user) return res.status(403).json({ error: 'Forbidden' });
  res.json(user);
});
Enter fullscreen mode Exit fullscreen mode

For role-based access, centralize your authorization logic in middleware or a dedicated policy layer — don't scatter if (user.role === 'admin') checks throughout your codebase.

2. Cryptographic Failures

Formerly: Sensitive Data Exposure

The rename is deliberate. The root cause isn't that data gets exposed — it's that cryptographic controls were either absent, weak, or misapplied.

Common Patterns

Storing passwords with MD5 or SHA-1 instead of bcrypt, Argon2, or scrypt
Transmitting sensitive data over HTTP instead of HTTPS
Using ECB mode for symmetric encryption (produces identical ciphertext for identical plaintext blocks)
Hardcoding encryption keys in source code

Password Hashing Done Right

python# ❌ Never do this

import hashlib
hashed = hashlib.md5(password.encode()).hexdigest()

# ✅ Use a proper adaptive hashing function
import bcrypt

def hash_password(plain_text: str) -> str:
    salt = bcrypt.gensalt(rounds=12)  # Cost factor: tune based on server capacity
    return bcrypt.hashpw(plain_text.encode(), salt).decode()

def verify_password(plain_text: str, hashed: str) -> bool:
    return bcrypt.checkpw(plain_text.encode(), hashed.encode())
Enter fullscreen mode Exit fullscreen mode

The rounds parameter in bcrypt is crucial. At rounds=12, a modern server takes roughly 250ms to hash a password — slow enough to defeat brute-force attacks, fast enough to be imperceptible to users.

TLS Configuration

Don't just enable HTTPS. Audit your TLS configuration with tools like SSL Labs. Disable TLS 1.0 and 1.1. Enforce strong cipher suites. Enable HSTS with a long max-age.

nginx# Nginx: enforce modern TLS

ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers off;
add_header Strict-Transport-Security "max-age=63072000" always;
Enter fullscreen mode Exit fullscreen mode

3. Injection

SQL, NoSQL, Command, LDAP, and more

Injection vulnerabilities occur when untrusted data is sent to an interpreter as part of a command or query. SQL injection remains one of the most destructive attack vectors in existence despite being entirely preventable.

SQL Injection: Still Killing Systems in 2024

python# ❌ Classic SQL injection vulnerability

def get_user(username):
    query = f"SELECT * FROM users WHERE username = '{username}'"
    return db.execute(query)
Enter fullscreen mode Exit fullscreen mode

This returns all users

The Parameterized Query Pattern

python# ✅ Parameterized queries — the only correct approach

def get_user(username):
    query = "SELECT * FROM users WHERE username = ?"
    return db.execute(query, (username,))
Enter fullscreen mode Exit fullscreen mode

With an ORM like SQLAlchemy:

python# ✅ ORM queries are parameterized by default
user = session.query(User).filter(User.username == username).first()

Command Injection

Command injection is often overlooked in non-database contexts:

python# ❌ Passing user input to shell commands

import subprocess
filename = request.args.get('file')
result = subprocess.run(f"cat /reports/{filename}", shell=True, capture_output=True)`

Attack: filename = "../../etc/passwd"
Or: filename = "report.pdf; rm -rf /"
Enter fullscreen mode Exit fullscreen mode

python# ✅ Use argument lists, never shell=True with user input

import subprocess, os, pathlib

def safe_read_report(filename: str) -> bytes:
    # Validate the filename is alphanumeric with allowed extensions
    safe_name = pathlib.Path(filename).name  # Strip directory traversal
    report_path = pathlib.Path('/reports') / safe_name

    if not report_path.suffix in ['.pdf', '.csv']:
        raise ValueError("Invalid file type")

    result = subprocess.run(['cat', str(report_path)], capture_output=True, shell=False)
    return result.stdout
Enter fullscreen mode Exit fullscreen mode

4. Insecure Design

The Newest Category (Added 2021)

This is the only category that can't be fixed with a code patch. Insecure design refers to missing or ineffective security controls in the architecture itself — decisions made (or not made) before a line of code was written.

What This Looks Like

A password reset flow that uses predictable tokens
An API with no rate limiting on authentication endpoints
A multi-tenant SaaS that stores all customer data in a single database schema without row-level isolation
A file upload feature with no content-type validation or sandboxed storage

Threat Modeling as a Practice

Before writing code for any new feature, spend 30 minutes on a lightweight threat model. Ask:

What are we building? Map the data flows and trust boundaries.
What can go wrong? Use the STRIDE framework (Spoofing, Tampering, Repudiation, Information Disclosure, Denial of Service, Elevation of Privilege).
What are we going to do about it? Document the controls.
Did we do a good enough job? Validate in code review.

This is particularly important for teams building custom web platforms. Development shops handling bespoke web projects — like those offering website development in Glendale Heights, Illinois — need threat modeling baked into the design phase, not as an afterthought, especially when building systems that handle customer PII or payment data.

5. Security Misconfiguration

The Broadest Category

Security misconfiguration is the gap between a system's secure capability and its actual configuration. It's also one of the easiest vulnerabilities to prevent with proper DevSecOps practices.

The Most Common Offenders

Default credentials:
Many databases, admin panels, and IoT devices ship with default usernames and passwords. Scanners actively search for these.

Verbose error messages in production:

python# ❌ Leaking stack traces to end users

@app.errorhandler(Exception)
def handle_error(e):
    return jsonify({"error": str(e), "traceback": traceback.format_exc()}), 500

# ✅ Log internally, return generic messages externally
@app.errorhandler(Exception)
def handle_error(e):
    app.logger.error(f"Unhandled exception: {e}", exc_info=True)
    return jsonify({"error": "An internal error occurred"}), 500
Enter fullscreen mode Exit fullscreen mode

Unnecessary HTTP headers:

nginx# Disable server version disclosure
server_tokens off;

# Add security headers
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options DENY;
add_header Content-Security-Policy "default-src 'self'";
add_header Referrer-Policy "strict-origin-when-cross-origin";
Enter fullscreen mode Exit fullscreen mode

Cloud storage buckets: S3 buckets and GCS buckets should never be publicly readable unless explicitly intended. Use infrastructure-as-code (Terraform, Pulumi) with security group policies enforced at the IaC level, not manually through consoles.

6. Vulnerable and Outdated Components

Supply Chain Risk Is Real

Modern applications import hundreds of third-party packages. Each dependency is a potential attack surface. The 2021 Log4Shell vulnerability, a remote code execution bug in a ubiquitous Java logging library affected millions of applications that simply had Log4j in their dependency tree.

Practical Mitigation

Audit your dependencies regularly:

bash# Node.js

npm audit
npm audit fix
Enter fullscreen mode Exit fullscreen mode

Python

pip install pip-audit
pip-audit
Enter fullscreen mode Exit fullscreen mode

Java (Maven)

mvn dependency-check:check

Pin versions and lock files:

json// package.json  use exact versions for critical deps
{
  "dependencies": {
    "express": "4.18.2",
    "jsonwebtoken": "9.0.0"
  }
}
Enter fullscreen mode Exit fullscreen mode

Commit package-lock.json, yarn.lock, or poetry.lock. These lock files ensure deterministic builds and prevent silent dependency upgrades.

Use Software Composition Analysis (SCA) tools like Snyk, Dependabot, or OWASP Dependency-Check in your CI/CD pipeline. Every pull request should be scanned.

  1. Identification and Authentication Failures

Weak Auth Is an Open Door

Formerly "Broken Authentication," this category covers flaws in session management, credential policies, and identity verification that allow attackers to impersonate legitimate users.

JWT Pitfalls

JWTs are widely used but often implemented incorrectly:

javascript// ❌ The "alg: none" attack — never allow this
jwt.verify(token, secret, { algorithms: ['HS256', 'none'] });

// ❌ Symmetric secret is too short/guessable
const token = jwt.sign(payload, 'secret123');

// ✅ Explicitly whitelist algorithms, use strong secrets
const token = jwt.sign(payload, process.env.JWT_SECRET, {
  algorithm: 'HS256',
  expiresIn: '15m'  // Short-lived tokens reduce exposure window
});

jwt.verify(token, process.env.JWT_SECRET, {
  algorithms: ['HS256']  // Explicit whitelist
});
Enter fullscreen mode Exit fullscreen mode

Multi-Factor Authentication

For any application handling sensitive data or financial operations, MFA should be mandatory for privileged accounts and strongly encouraged for all users. Implement TOTP (Time-based One-Time Passwords) using libraries like speakeasy (Node.js) or pyotp (Python) rather than SMS, which is vulnerable to SIM-swapping attacks.

Rate Limiting on Auth Endpoints

python

from flask_limiter import Limiter

limiter = Limiter(app, key_func=get_remote_address)

@app.route('/auth/login', methods=['POST'])
@limiter.limit("5 per minute")  # Brute-force protection
def login():
Enter fullscreen mode Exit fullscreen mode

8. Software and Data Integrity Failures

Deserialization and CI/CD Pipeline Attacks

This category covers scenarios where code or data is assumed to be from a trusted source without verification. Two primary vectors: insecure deserialization and compromised CI/CD pipelines.

Insecure Deserialization

python# ❌ Never deserialize untrusted data with pickle

import pickle
data = pickle.loads(user_supplied_bytes)  # Arbitrary code execution
Enter fullscreen mode Exit fullscreen mode

✅ Use safe, schema-validated formats

import json
from pydantic import BaseModel

class UserInput(BaseModel):
    name: str
    age: int

data = UserInput.model_validate_json(user_supplied_string)

Enter fullscreen mode Exit fullscreen mode

Pipeline Integrity

Verify the integrity of artifacts in your build pipeline. Use checksums and signature verification for downloaded binaries and packages. In GitHub Actions:

yaml- name: Setup Node.js
  uses: actions/setup-node@v3  # Pin to a specific SHA for supply chain security
  # Consider: uses: actions/setup-node@1f8c7b [full SHA]
  with:
    node-version: '20'
Enter fullscreen mode Exit fullscreen mode
  1. Security Logging and Monitoring Failures

You Can't Respond to What You Can't See

The average time to detect a breach is still measured in months. Insufficient logging is a primary reason. This category isn't just about logging errors — it's about logging security-relevant events with enough context to reconstruct what happened.

What to Log

python

import structlog

logger = structlog.get_logger()

# Log authentication events
def authenticate(username, success, ip_address, user_agent):
    logger.info(
        "authentication_attempt",
        username=username,
        success=success,
        ip=ip_address,
        user_agent=user_agent,
        timestamp=datetime.utcnow().isoformat()
    )

# Log access control failures
def check_permission(user_id, resource_id, action, granted):
    if not granted:
        logger.warning(
            "access_control_failure",
            user_id=user_id,
            resource_id=resource_id,
            action=action
        )
Enter fullscreen mode Exit fullscreen mode

What Not to Log

Never log passwords, tokens, credit card numbers, or full request bodies that may contain sensitive data. Implement structured logging (JSON) and ship logs to a centralized SIEM — not just local files that can be wiped.

  1. Server-Side Request Forgery (SSRF)

Attacking Internal Infrastructure Through Your Own Server

SSRF occurs when an attacker can cause your server to make HTTP requests to an arbitrary destination — including internal services that aren't exposed to the internet.

A Classic SSRF Scenario

python# ❌ Fetching a URL provided directly by user input

@app.route('/fetch')
def fetch_url():
    url = request.args.get('url')
    response = requests.get(url)  # The server makes this request
    return response.text

Attack: /fetch?url=http://169.254.169.254/latest/meta-data/
On AWS, this retrieves EC2 instance metadata, including IAM credentials
Enter fullscreen mode Exit fullscreen mode

Mitigation

python

import ipaddress
from urllib.parse import urlparse

ALLOWED_SCHEMES = {'https'}
BLOCKED_NETWORKS = [
    ipaddress.ip_network('10.0.0.0/8'),
    ipaddress.ip_network('172.16.0.0/12'),
    ipaddress.ip_network('192.168.0.0/16'),
    ipaddress.ip_network('169.254.0.0/16'),  # Link-local (AWS metadata)
    ipaddress.ip_network('127.0.0.0/8'),
]

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

    if parsed.scheme not in ALLOWED_SCHEMES:
        return False

    try:
        import socket
        ip = ipaddress.ip_address(socket.gethostbyname(parsed.hostname))
        for network in BLOCKED_NETWORKS:
            if ip in network:
                return False
    except Exception:
        return False

    return True
Enter fullscreen mode Exit fullscreen mode

Note: DNS rebinding attacks can bypass hostname-based checks. For production systems, implement network-level egress filtering to block outbound connections to private IP ranges at the infrastructure level.

Integrating OWASP Into Your Development Workflow

Understanding the Top 10 is only the first step. The goal is to make secure coding a default behavior, not a checklist item before deployment.

Shift-Left Security Practices

  1. Pre-commit hooks: Catch secrets and obvious vulnerabilities before they enter version control.
bash#
.pre-commit-config.yaml
repos:
  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.16.1
    hooks:
      - id: gitleaks
  - repo: https://github.com/PyCQA/bandit
    rev: 1.7.5
    hooks:
      - id: bandit
        args: ["-c", "pyproject.toml"]
Enter fullscreen mode Exit fullscreen mode
  1. SAST in CI: Integrate static analysis tools — Semgrep, SonarQube, or Bandit — into your pull request pipeline. Every merge should be scanned.

  2. Dependency scanning: As covered in #6, automate this. Don't rely on developers manually running npm audit.

  3. Security in code review: Train reviewers to look for OWASP patterns. Create a security checklist specific to your tech stack.

The OWASP ASVS

For teams that need more granular requirements, the OWASP Application Security Verification Standard (ASVS) provides three levels of security requirements. Level 1 is a baseline for all applications; Level 2 is for applications handling sensitive data; Level 3 is for high-value targets.

Building Secure by Default: Architecture Patterns

Security-first architecture reduces the blast radius when vulnerabilities do occur.

Defense in Depth

Don't rely on a single control. Layer your defenses:

Network layer: WAF, DDoS protection, VPC with private subnets
Application layer: Input validation, output encoding, parameterized queries
Data layer: Encryption at rest, minimal privilege DB accounts, row-level security
Monitoring layer: Centralized logging, anomaly detection, alerting

The Principle of Least Privilege

Every component — database accounts, IAM roles, service accounts — should have the minimum permissions required to function. A read-only API endpoint should use a database account with SELECT permissions only. A Lambda function that writes to S3 should have a policy scoped to exactly that bucket and action.

For teams building production web applications, especially custom business platforms such as businesses seeking website development in O'Fallon, Illinois applying least-privilege principles at the infrastructure level is just as important as securing application code, particularly when the platform handles customer records or payment workflows.

A Practical Security Checklist Before You Deploy

Before any production deployment, run through this:

  • All SQL queries use parameterized statements or ORM abstractions
  • User-supplied input is validated at the API boundary (type, length, format).
  • Passwords are hashed with bcrypt, Argon2, or scrypt (not MD5/SHA-1)
  • JWT algorithms are explicitly whitelisted; tokens expire in ≤ 15 minutes.
  • Authentication endpoints are rate-limited.
  • Error responses don't leak stack traces or internal paths.
  • Security headers are configured (CSP, HSTS, X-Frame-Options).
  • npm audit / pip-audit returns no high/critical vulnerabilities.
  • S3 buckets, GCS buckets, and Azure Blob containers are not publicly readable.
  • Database accounts use minimum required permissions.
  • Logs capture authentication events and access control failures.
  • SSRF protections are in place for any server-side HTTP fetch functionality.
  • Secrets are stored in a vault (AWS Secrets Manager, HashiCorp Vault) — not in environment files committed to source control.

Final Thought

The OWASP Top 10 isn't a compliance checkbox. It's a distillation of the most common ways real applications get compromised. The categories haven't changed dramatically in years because the industry keeps making the same mistakes not because they're hard to fix, but because developers often encounter them too late in the process.

Security knowledge compounds. Once you understand why parameterized queries work at the driver level, you stop reaching for string interpolation reflexively. Once you've traced an IDOR vulnerability through a codebase, you start thinking about object ownership every time you write an API endpoint.

Top comments (0)