OWASP Top 10 Explained: What Developers Actually Need to Understand
A developer-focused explanation of the OWASP Top 10 web application security risks — what each means in practice, why it happens, and how to prevent it in your code.

James Ross Jr.
Strategic Systems Architect & Enterprise Software Developer
OWASP Top 10 Explained: What Developers Actually Need to Understand
The OWASP Top 10 is the security industry's most-cited reference for web application vulnerabilities. Most developers have heard of it. Far fewer have actually read the full documentation and understood what each category means in terms of real code they write every day.
I am going to skip the abstract descriptions and focus on what each category means in practice — the actual code patterns that create vulnerabilities and the concrete changes that prevent them.
A01: Broken Access Control
This is the top vulnerability for a reason. Broken access control means your application does not properly enforce what authenticated users are allowed to do.
The classic example: your API has an endpoint GET /api/orders/:orderId that returns order details. Your authentication middleware verifies the user is logged in. But does your handler verify the order belongs to the logged-in user?
// Vulnerable
app.get("/api/orders/:orderId", authenticate, async (req, res) => {
const order = await db.order.findById(req.params.orderId);
res.json(order); // Returns any order if you know the ID
});
// Correct
app.get("/api/orders/:orderId", authenticate, async (req, res) => {
const order = await db.order.findFirst({
where: {
id: req.params.orderId,
userId: req.user.id, // Enforces ownership
},
});
if (!order) return res.status(404).json({ error: "Not found" });
res.json(order);
});
This is called Insecure Direct Object Reference (IDOR). An attacker changes the orderId parameter to access other users' orders. Always filter database queries by the authenticated user's context.
A02: Cryptographic Failures
Formerly called "Sensitive Data Exposure" — this category is about failing to protect data that needs cryptographic protection.
The most common failure: using a weak hashing algorithm for passwords.
// Vulnerable — MD5 is fast and reversible via rainbow tables
const hash = crypto.createHash("md5").update(password).digest("hex");
// Correct — bcrypt is slow by design, resistant to rainbow tables
const hash = await bcrypt.hash(password, 12); // 12 rounds
Also in this category: transmitting sensitive data over HTTP instead of HTTPS, storing credit card numbers in plaintext, using deprecated SSL/TLS versions, and not encrypting database fields that contain sensitive personal information.
Use HTTPS everywhere. Hash passwords with bcrypt, Argon2id, or scrypt. Encrypt sensitive data fields at rest. Never log passwords, tokens, or card numbers.
A03: Injection
SQL injection remains a top vulnerability despite being one of the oldest known attack types. The mechanism: unsanitized user input is incorporated into a SQL query and interpreted as SQL syntax.
// Vulnerable
const query = `SELECT * FROM users WHERE email = '${req.body.email}'`;
// If email is: ' OR '1'='1, query returns all users
// Correct — parameterized query
const user = await db.query(
"SELECT * FROM users WHERE email = $1",
[req.body.email]
);
This applies to every injection context: SQL, NoSQL, LDAP, XML, operating system commands. The fix is consistent: never concatenate user input into interpreted strings. Use parameterized queries for databases, safe APIs for OS interaction, and validated formats for everything else.
Modern ORMs (Prisma, TypeORM, Sequelize) use parameterized queries by default. The vulnerability appears when developers bypass the ORM to write raw SQL, often for performance reasons. When you use raw queries, parameterize them.
A04: Insecure Design
This category covers architectural security failures — vulnerabilities that result from how the system was designed, not just how it was implemented. No amount of code-level fixes can resolve insecure design.
Example: a password reset flow that sends a 4-digit numeric code via email. An attacker can brute-force 10,000 possible codes, especially if there is no rate limiting on the verification endpoint. The design is insecure regardless of how correctly the implementation is written.
Insecure design requires design-level fixes: use cryptographically random tokens instead of short codes, add rate limiting, add exponential backoff after failed attempts, make codes expire quickly.
Prevention requires threat modeling during design, not security review after implementation. For each user-facing feature, identify misuse cases: what would a malicious user try to do with this? Design to prevent it.
A05: Security Misconfiguration
Insecure default configurations, unchanged default credentials, verbose error messages exposing stack traces, unnecessary features enabled, missing security headers — these are all security misconfiguration.
Common examples I find in production: debug mode enabled in production (exposing internal state), default admin credentials unchanged, directory listing enabled in web servers, cloud storage buckets publicly accessible by default, error pages returning stack traces to users.
The fix is environment-specific configuration that locks down production appropriately. Production should have debug mode disabled, verbose logging disabled, default credentials changed, minimal services enabled, and security headers configured. Automate this configuration verification.
A06: Vulnerable and Outdated Components
Using libraries with known vulnerabilities. Every npm install pulls in dependencies, and those dependencies have dependencies. Any of them may have known CVEs.
# Audit your dependencies
npm audit
# Fix automatically fixable issues
npm audit fix
Run npm audit in CI and fail the build on high-severity vulnerabilities without available fixes disabled. Enable Dependabot or Renovate to automatically create PRs when dependencies have updates available.
This is not just about your direct dependencies. Your entire dependency tree is your attack surface. A critical CVE in a package three levels deep in your dependency graph still affects you.
A07: Identification and Authentication Failures
Weak password policies, no multi-factor authentication, session tokens that do not expire, concurrent session handling vulnerabilities, credential stuffing with no detection.
The minimum baseline for production applications: require passwords of at least 12 characters, use bcrypt/Argon2 for hashing, expire sessions after inactivity, implement rate limiting on authentication endpoints, support MFA (TOTP or passkeys), and invalidate sessions on logout.
Credential stuffing — using leaked credential lists from data breaches to try username/password combinations on your application — is increasingly automated and effective. Implement rate limiting, CAPTCHA on repeated failures, and breach password detection (Have I Been Pwned API) to detect and block these attacks.
A08: Software and Data Integrity Failures
This category covers failures to verify the integrity of software or data. The most relevant example for web developers: allowing deserialization of untrusted data without validation.
If your application deserializes user-supplied data into objects (common in session storage, job queues, or API requests), validate the structure before using it. Unvalidated deserialization can allow attackers to manipulate object properties in ways that execute unintended code paths.
Also in this category: CI/CD pipelines with insecure configuration, dependencies pulled from untrusted sources, and automatic updates without integrity verification. Pin your package versions, verify checksums, and use lockfiles.
A09: Security Logging and Monitoring Failures
Not logging security-relevant events, or logging them in ways that are not actionable. Failed authentication attempts, access control violations, and high-value data access should be logged with enough context to reconstruct what happened.
// Log security events with context
logger.warn({
event: "authentication.failed",
username: req.body.username,
ip: req.ip,
userAgent: req.headers["user-agent"],
timestamp: new Date().toISOString(),
}, "Failed login attempt");
These logs need to be shipped to a centralized system and monitored. 100 failed login attempts in 5 minutes from the same IP is a brute-force attempt — your monitoring should alert on it. A user accessing 500 records in 10 minutes is unusual behavior that might indicate data exfiltration.
A10: Server-Side Request Forgery (SSRF)
SSRF occurs when your application fetches a URL provided by a user and the request goes somewhere unintended — often to internal services that are not accessible from the internet.
// Vulnerable — fetches any URL including internal ones
app.get("/proxy", async (req, res) => {
const response = await fetch(req.query.url as string);
res.send(await response.text());
});
// An attacker can request: http://169.254.169.254/latest/meta-data/
// (AWS instance metadata) and get cloud credentials
Validate and restrict URLs before fetching them. Block private IP ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8, 169.254.0.0/16). Use an allowlist of permitted domains rather than a blocklist if possible. Use network-level controls to prevent your application server from reaching internal resources.
Understanding the OWASP Top 10 is the starting point. Implementing mitigations consistently across a codebase requires experience and ongoing attention. If you want help auditing your application against these vulnerabilities, book a session at https://calendly.com/jamesrossjr.