If you've ever shipped a feature only to get a security ticket two weeks later about IDOR or SQL injection, you're not alone. Most OWASP Top 10 issues aren't exotic zero-days—they're the same mistakes developers have been making for years, just in new frameworks.

This tutorial walks through each of the ten risks in the OWASP Top 10:2025 edition with code examples, GitHub references, and practical exercises you can try today. No fluff, no marketing—just the stuff you need to write safer code.
What Is the OWASP Top 10?
The OWASP Top 10 is a standard awareness document that ranks the ten most critical security risks to web applications. It's published by the Open Worldwide Application Security Project (OWASP), a nonprofit foundation, and is rebuilt roughly every three to four years from data on hundreds of thousands of real applications.
Each entry is a category, not a single bug. "Injection" covers SQL injection, XSS, command injection, and more—all grouped because they share a root cause: untrusted input reaching an interpreter. That grouping is what makes the list useful. You don't memorize ten thousand bugs; you understand ten patterns and recognize their variations.
The 2025 edition is the most current version. It adds two new categories (Software Supply Chain Failures and Mishandling of Exceptional Conditions), folds SSRF into Broken Access Control, and moves Security Misconfiguration up to number two.
A01: Broken Access Control
Broken access control is when an application fails to enforce what a user is allowed to do, letting them read or change data that should be off limits. It's been #1 since 2021 and stayed there in 2025.
The Classic IDOR
The textbook example is an Insecure Direct Object Reference (IDOR). You view your invoice at /invoice?id=1043, change the number to 1044, and the app hands you someone else's invoice because it checked you were logged in but never checked the record belonged to you.
Vulnerable code (Node.js/Express):
// ❌ Vulnerable — no ownership check
app.get('/api/orders/:id', authenticate, async (req, res) => {
const order = await Order.findById(req.params.id);
res.json(order);
});
Fixed code:
// ✅ Fixed — verify the order belongs to the requesting user
app.get('/api/orders/:id', authenticate, async (req, res) => {
const order = await Order.findOne({
_id: req.params.id,
userId: req.user.id // ownership enforced at query level
});
if (!order) return res.status(404).json({ error: 'Not found' });
res.json(order);
});
The gotcha junior devs miss: denying access in the UI doesn't count. If you hide a "delete" button but the DELETE /api/users/123 endpoint has no server-side role check, any authenticated user with curl can hit it.
SSRF Is Now Part of A01
In 2025, Server-Side Request Forgery (SSRF) moved into Broken Access Control as a sub-case. SSRF tricks your server into making requests to internal services (like AWS metadata at http://169.254.169.254/latest/meta-data/) that should never be reachable from the outside.
Vulnerable code:
// ❌ Vulnerable — fetches any user-supplied URL
app.post('/webhook', async (req, res) => {
const response = await fetch(req.body.webhookUrl);
res.json(await response.json());
});
Fixed code:
const { URL } = require('url');
function isSafeUrl(input) {
try {
const url = new URL(input);
const blocked = ['169.254.169.254', 'localhost', '127.0.0.1', '0.0.0.0'];
if (blocked.some(h => url.hostname.includes(h))) return false;
if (!['http:', 'https:'].includes(url.protocol)) return false;
return true;
} catch {
return false;
}
}
// ✅ Validate before fetching any user-supplied URL
app.post('/webhook', async (req, res) => {
if (!isSafeUrl(req.body.webhookUrl)) {
return res.status(400).json({ error: 'Invalid URL' });
}
const response = await fetch(req.body.webhookUrl);
res.json(await response.json());
});
The non-obvious gotcha: blocking 127.0.0.1 is not enough. Attackers use decimal IP notation (http://2130706433/ is 127.0.0.1), IPv6 loopback (::1), and DNS rebinding to bypass naive blocklists. Validate after DNS resolution, not before.
GitHub Examples
- OWASP Top 10:2025 Checklist — 249 CWEs with descriptions, examples, and prevention guidance.
- Agent Skills Security Checklist — Prevention tips for each Top 10 category.
Practical Exercise
Set up a local app with two roles (user/admin). Create an endpoint that returns data based on an ID parameter. Try accessing another user's data by changing the ID. Then add server-side ownership checks and verify the attack no longer works.
Troubleshooting
- Problem: "I added auth middleware but still getting unauthorized access."
- Fix: Auth middleware only checks if the user is logged in. You need explicit ownership or role checks in each route handler.
Best Practices
- Enforce ownership at the query level, not just in application logic.
- Deny by default; explicitly grant access based on roles.
- Test every route handler with different user roles.
Performance Tips
- Cache ownership checks where possible (e.g., user roles in JWT claims).
- Use database-level constraints (e.g.,
WHERE owner_id = ?) to prevent accidental leaks.
Common Errors
- Hiding UI elements without server-side checks.
- Assuming authentication equals authorization.
- Forgetting to check ownership on
PUT,PATCH, andDELETEendpoints.
A02: Security Misconfiguration
Security misconfiguration is a system left in an insecure state through defaults, oversights, or unnecessary features rather than a coding bug. It climbed from fifth in 2021 to second in 2025.
Real-World Examples
- Default credentials left on an admin console (e.g.,
admin/admin). - Cloud storage buckets set to public.
- Directory listing enabled on web servers.
- Verbose error pages that leak stack traces.
- Exposed
.gitfolders handing attackers your whole source tree.
The 2019 Capital One breach was misconfiguration—an SSRF vulnerability combined with an overly permissive IAM role. The root issue wasn't the SSRF; it was that the role had permissions it never needed.
Concrete Actions
- Set
NODE_ENV=production—many frameworks disable security features in development mode. - Never return raw stack traces to clients.
- Use CSP headers (try the CSP Generator to build them).
- Audit IAM roles with least-privilege in mind.
GitHub Examples
- OWASP Security Misconfiguration Guide — Official examples and checklists.
Practical Exercise
Spin up a default installation of a web app (e.g., WordPress, Jenkins). Find the default admin credentials (often admin/admin). Change them and disable directory listing.
Troubleshooting
- Problem: "My app is leaking stack traces in production."
- Fix: Set your framework to production mode and configure custom error pages that don't expose internals.
Best Practices
- Harden default configurations before deployment.
- Remove unused features, endpoints, and documentation.
- Regularly audit cloud IAM policies.
Performance Tips
- Use automated configuration scanners (e.g., Nuclei) in CI/CD.
- Document secure baselines for each environment.
Common Errors
- Leaving debug endpoints enabled in production.
- Using default passwords on admin panels.
- Exposing
.envfiles or.gitdirectories.
A03: Software Supply Chain Failures
Software supply chain failures cover risks introduced by the components, dependencies, and build pipelines your application relies on rather than code your team wrote. In 2025, OWASP broadened the old "Vulnerable and Outdated Components" category into the full supply chain.
Real-World Examples
- A dependency with a known CVE that you never patched. Log4Shell (CVE-2021-44228) took down organizations worldwide because a logging library had an unauthenticated RCE.
- A maintainer account gets phished and a malicious version of a popular npm package ships to everyone who runs an update.
Your code can be perfect and you still get owned through a package you never read.
Prevention
Run these in your CI pipeline and fail the build on high-severity findings:
# Node.js
npm audit
# Python
pip-audit
# OWASP Dependency-Check (multi-language)
dependency-check -p my-app -s ./lib
GitHub Examples
- OWASP Dependency-Check — Automated SCA tool.
- OWASP Top 10:2025 Checklist — Includes supply chain testing guidance.
Practical Exercise
Add npm audit or pip-audit to your CI pipeline. Set it to fail on high-severity findings. Run it on an existing project and fix at least one vulnerable dependency.
Troubleshooting
- Problem: "My CI pipeline is failing because of a transitive dependency."
-
Fix: Use
npm ls <package>orpip show <package>to find the parent dependency. Update or replace the parent.
Best Practices
- Pin dependency versions (e.g.,
express@4.18.2, not^4.18.2). - Use lockfiles (
package-lock.json,Pipfile.lock). - Verify package signatures where possible.
Performance Tips
- Cache dependency scans to speed up CI.
- Use tools like Renovate or Dependabot for automated updates.
Common Errors
- Ignoring
npm auditwarnings. - Using
latesttags in production. - Not auditing transitive dependencies.
A04: Cryptographic Failures
Cryptographic failures happen when sensitive data is not protected properly, whether through weak algorithms, missing encryption, or bad key handling. It sat at second in 2021 and moved to fourth in 2025.
Real-World Examples
- Storing passwords with MD5 or SHA-1. An application that stores passwords as unsalted MD5 hashes has effectively stored them in plaintext.
- Encrypting data with AES-ECB mode (reveals patterns in identical plaintext blocks).
- Using a static IV with AES-CBC.
- Transmitting data over plain HTTP.
- Hardcoding encryption keys in source code.
Prevention
Vulnerable code:
const crypto = require('crypto');
// ❌ Vulnerable — MD5 is not a password hash
const hash = crypto.createHash('md5').update(password).digest('hex');
Fixed code:
const bcrypt = require('bcrypt');
// ✅ Fixed — bcrypt with a cost factor of 12+
const hash = await bcrypt.hash(password, 12);
const valid = await bcrypt.compare(inputPassword, hash);
GitHub Examples
- OWASP Cryptographic Failures Guide — Official examples.
Practical Exercise
Find a login form in your app. Check how passwords are hashed. If it's MD5, SHA-1, or unsalted SHA-256, migrate to bcrypt or Argon2.
Troubleshooting
- Problem: "My bcrypt hashes are too slow."
- Fix: Adjust the cost factor. Start at 10 for development, 12+ for production. Test login latency.
Best Practices
- Use bcrypt, Argon2, or scrypt for passwords.
- Always use HTTPS in production.
- Rotate encryption keys periodically.
Performance Tips
- Cache bcrypt hashes where possible (e.g., session tokens).
- Use hardware security modules (HSMs) for key storage in high-security environments.
Common Errors
- Using MD5 or SHA-1 for passwords.
- Storing encryption keys in code or
.envfiles. - Using ECB mode for encryption.
A05: Injection
Injection is when untrusted input is interpreted as a command or query, letting an attacker change what the application does. It includes SQL injection, XSS, command injection, LDAP injection, and NoSQL injection. It dropped from third in 2021 to fifth in 2025 as frameworks made the common cases harder, but it has not gone away.
SQL Injection
Vulnerable code:
// ❌ Vulnerable SQL injection
const query = `SELECT * FROM users WHERE email = '${req.body.email}'`;
Fixed code:
// ✅ Fixed — parameterized query
const result = await db.query(
'SELECT * FROM users WHERE email = $1',
[req.body.email]
);
NoSQL Injection (MongoDB)
Vulnerable code:
// ❌ Vulnerable NoSQL injection
User.findOne({ username: req.body.username, password: req.body.password });
// Attacker sends: { "password": { "$gt": "" } }
Fixed code:
// ✅ Fixed — validate input type before querying
if (typeof req.body.password !== 'string') return res.status(400).end();
User.findOne({ username: req.body.username, password: req.body.password });
XSS (Cross-Site Scripting)
Vulnerable code (React):
// ❌ Vulnerable — dangerouslySetInnerHTML
<div dangerouslySetInnerHTML={{ __html: userComment }} />
Fixed code:
// ✅ Fixed — let React escape by default
<div>{userComment}</div>
GitHub Examples
- OWASP Injection Guide — Official examples.
- SQLMap — Automated SQL injection tool.
Practical Exercise
Set up a login form vulnerable to SQL injection. Use the payload ' OR '1'='1 to bypass authentication. Then extract all user data using UNION-based injection.
Troubleshooting
- Problem: "My parameterized query is still vulnerable."
-
Fix: Ensure you're not concatenating user input anywhere in the query string. Use placeholders (
$1,?) exclusively.
Best Practices
- Use parameterized queries or ORM methods.
- Validate and sanitize all input.
- Use frameworks with built-in XSS protection (e.g., React, Vue).
Performance Tips
- Use prepared statements for repeated queries.
- Cache validated input schemas.
Common Errors
- Concatenating user input into SQL queries.
- Using
dangerouslySetInnerHTMLin React without sanitization. - Trusting NoSQL query objects from user input.
A06: Insecure Design
Insecure design is a flaw in the intended logic of an application rather than a mistake in its implementation. You can't patch it with a security header because the problem is the plan itself. It held at fourth in 2021 and sits at sixth in 2025.
Real-World Examples
- A password reset flow that sends a six-digit code but never limits how many guesses you get.
- A checkout that lets you apply the same single-use discount a thousand times.
- A transfer that accepts a negative amount and moves money the wrong way.
Prevention
The fix is threat modeling before you write a line of code. For every new feature, ask: what's the worst thing an attacker can do with this? STRIDE (Spoofing, Tampering, Repudiation, Information Disclosure, Denial of Service, Elevation of Privilege) is a useful checklist.
GitHub Examples
- OWASP Insecure Design Guide — Official examples.
- Threat Modeling Templates — OWASP templates for STRIDE.
Practical Exercise
Design a password reset flow. Add rate limiting (e.g., max 5 attempts per hour). Test by trying to brute-force the code.
Troubleshooting
- Problem: "My feature works but can be abused."
- Fix: Add business logic constraints (e.g., rate limits, one-time tokens, negative value checks).
Best Practices
- Threat model every new feature.
- Use the STRIDE framework.
- Test for business logic abuse, not just technical bugs.
Performance Tips
- Use in-memory rate limiters (e.g., Redis) for high-traffic apps.
- Cache threat model decisions for similar features.
Common Errors
- Skipping threat modeling for "simple" features.
- Assuming users will only use features as intended.
- Not testing edge cases (e.g., negative amounts, zero values).
A07: Authentication Failures
Authentication failures are weaknesses in how an application confirms who a user is and keeps them logged in. OWASP shortened the 2021 name "Identification and Authentication Failures" to just "Authentication Failures" in 2025, but the substance is the same. It stayed at seventh.
Real-World Examples
- Weak session tokens that never expire.
- Missing rate limits on login endpoints.
- Accepting JWTs signed with
alg: none.
The JWT alg: none attack is a real bypass—some libraries in 2015–2018 would accept an unsigned token if the header specified "alg": "none".
Prevention
Vulnerable code:
const jwt = require('jsonwebtoken');
// ❌ Vulnerable — trusts the algorithm from the token
const decoded = jwt.decode(token); // doesn't verify signature
Fixed code:
// ✅ Fixed — specify algorithm explicitly
const decoded = jwt.verify(token, process.env.JWT_SECRET, {
algorithms: ['HS256'] // reject anything else, including 'none'
});
GitHub Examples
- OWASP Authentication Failures Guide — Official examples.
Practical Exercise
Create a login endpoint. Try brute-forcing it with 100 requests. Add rate limiting (e.g., max 5 attempts per minute per IP).
Troubleshooting
-
Problem: "My JWT library accepts
alg: nonetokens." -
Fix: Explicitly specify allowed algorithms in
jwt.verify(). Never trust the algorithm from the token header.
Best Practices
- Use strong session tokens (e.g., 256-bit random values).
- Set session expiration times.
- Rate limit login endpoints.
Performance Tips
- Use Redis for distributed rate limiting.
- Cache JWT verification results for short-lived tokens.
Common Errors
- Accepting
alg: nonein JWTs. - Not expiring session tokens.
- Missing rate limits on authentication endpoints.
A08: Software or Data Integrity Failures
Software or data integrity failures occur when an application trusts code or data without verifying it has not been tampered with. The 2025 name swaps "and" for "or" but keeps the meaning from 2021. It stayed at eighth.
Real-World Examples
- Insecure deserialization (e.g., Python
pickle, PHPunserialize(), JavaObjectInputStream). - Auto-update mechanisms that fetch code over an unverified channel.
- CI/CD pipelines that deploy artifacts without checking a signature.
The SolarWinds attack is the textbook example—malicious code injected into a trusted build pipeline that then got signed and distributed to thousands of customers.
Prevention
Never deserialize untrusted data using Java's native ObjectInputStream, Python's pickle, or PHP's unserialize() without strict type checking. Use JSON with schema validation instead.
GitHub Examples
- OWASP Integrity Failures Guide — Official examples.
Practical Exercise
Create a Python endpoint that accepts a pickle-serialized object. Try sending a malicious payload that executes os.system('whoami'). Then switch to JSON with schema validation.
Troubleshooting
- Problem: "My app deserializes user input and crashes."
- Fix: Switch to JSON. If you must deserialize, use strict type checking and allowlists.
Best Practices
- Use JSON with schema validation (e.g., Joi, Pydantic).
- Sign and verify CI/CD artifacts.
- Use HTTPS for all update mechanisms.
Performance Tips
- Cache validated JSON schemas.
- Use binary serialization (e.g., Protocol Buffers) for high-performance needs.
Common Errors
- Deserializing untrusted data with
pickleorunserialize(). - Not verifying CI/CD artifact signatures.
- Fetching updates over HTTP.
A09: Security Logging and Alerting Failures
Security logging and alerting failures mean an application does not record security-relevant events or does not raise an alarm when something goes wrong, so attacks go unnoticed. OWASP renamed the 2021 "Monitoring" to "Alerting" in 2025 to stress that collecting logs is useless if nobody is watching them. It stayed at ninth.
Real-World Examples
- Not logging failed logins.
- Not logging access control decisions.
- Logging sensitive data (e.g., passwords, tokens, PII).
The average time to detect a breach is still measured in months. That's almost always a logging failure.
What to Log
- Authentication events (success and failure).
- Access control decisions.
- Input validation failures.
- Privilege escalation attempts.
What NOT to Log
- Passwords.
- Full credit card numbers.
- Session tokens.
- PII in URLs (e.g.,
/reset?token=abc123leaks the reset token).
GitHub Examples
- OWASP Logging Guide — Official examples.
Practical Exercise
Add logging to your login endpoint. Log success and failure (but not passwords). Set up an alert for more than 5 failed logins per minute from the same IP.
Troubleshooting
- Problem: "My logs are full of sensitive data."
- Fix: Use structured logging with field redaction (e.g., Winston with custom formatters).
Best Practices
- Log all security-relevant events.
- Redact sensitive fields before logging.
- Set up alerts for anomalous patterns.
Performance Tips
- Use asynchronous logging to avoid blocking requests.
- Sample high-volume logs (e.g., log 1 in 100 successful logins).
Common Errors
- Logging passwords or tokens.
- Not logging failed authentication attempts.
- Not setting up alerts for security events.
A10: Mishandling of Exceptional Conditions
Mishandling of exceptional conditions is a brand new 2025 category covering what happens when an application meets an error or an unexpected state and handles it insecurely. It replaces SSRF in the tenth slot, since SSRF moved up into Broken Access Control.
Real-World Examples
- Fail-open logic: an authorization check throws an exception when a service is unreachable, and the surrounding code treats the exception as "allow" instead of "deny".
- Verbose error responses that leak stack traces with database credentials.
- Unhandled exceptions that leave the application in a half-processed, exploitable state.
Prevention
Always fail closed on errors. If an authorization check fails or throws an exception, deny access by default.
Vulnerable code:
try {
const allowed = await checkPermission(userId, resourceId);
if (allowed) return res.json(data);
} catch (err) {
// ❌ Vulnerable — fail-open on error
return res.json(data);
}
Fixed code:
try {
const allowed = await checkPermission(userId, resourceId);
if (allowed) return res.json(data);
} catch (err) {
// ✅ Fixed — fail-closed on error
return res.status(500).json({ error: 'Service unavailable' });
}
GitHub Examples
- OWASP Exceptional Conditions Guide — Official examples.
Practical Exercise
Create an endpoint that depends on an external service. Simulate the service being down. Verify your app denies access instead of failing open.
Troubleshooting
- Problem: "My app allows access when a service is down."
- Fix: Wrap authorization checks in try-catch blocks and deny access on errors.
Best Practices
- Fail closed on all errors.
- Use custom error pages that don't leak internals.
- Log exceptions for debugging.
Performance Tips
- Use circuit breakers for external services.
- Cache authorization decisions where possible.
Common Errors
- Failing open on authorization errors.
- Leaking stack traces in error responses.
- Not handling edge cases (e.g., null values, empty arrays).
Learning Resources
Official OWASP Resources
- OWASP Top 10:2025 — Official documentation.
- OWASP Web Security Testing Guide — Detailed testing methodology.
- OWASP Application Security Verification Standard (ASVS) — Verification checklist.
Hands-On Labs
- HackerDNA Web Attacks Course — Browser-based OWASP Top 10 labs.
- DVWA (Damn Vulnerable Web App) — Local vulnerable app for practice.
- PortSwigger Web Security Academy — Free XSS, SQLi, and more labs.
GitHub Repositories
- OWASP Top 10:2025 Checklist — 249 CWEs with examples.
- Agent Skills Security Checklist — Prevention tips.
- Claude Code OWASP Skill — OWASP Top 10:2025, ASVS 5.0, and language-specific quirks.
Books and Courses
- The Web Application Hacker's Handbook — Classic reference.
- OWASP Testing Guide — Free online.
- Cyber security course in Bangalore BTM layout — Local training options for hands-on learning.
Final Thoughts
Pick one item from this list right now and check your current project against it. Broken Access Control is the highest-value target—search your codebase for every route handler and verify each one has an explicit ownership or role check, not just an authentication middleware. That single audit will surface more real vulnerabilities than any automated scanner you run this week.
The OWASP Top 10 is the map, not the territory. Memorizing the ten names gets you through a quiz; recognizing broken access control in a live application when the only clue is a number that increments takes practice. Every risk on this list is a pattern you learn to see by exploiting it, failing, and trying again until the shape of the flaw is obvious.
Start with one lab, one endpoint, one fix. Then move to the next. That's how you build secure software—one commit at a time.
If you found this useful, share it with your team. Better yet, run through one of the practical exercises together in your next security review. Happy coding, and stay secure. 🛡️
Top comments (0)