EncryptCodecencryptcodec
Blog/Web Security
Web SecurityApril 8, 2026 · 12 min read

OWASP Top 10 2025 Explained — With Real Code Examples for Developers

You ship a feature, security reviews it two weeks later, and suddenly there's a Jira ticket saying your new endpoint is vulnerable to IDOR. Sound familiar? Most OWASP Top 10 issues aren't exotic — they're the same mistakes developers have been making for 15 years, just in new frameworks.

Here's the practical breakdown: fix Broken Access Control first (it's #1 for a reason), audit your dependencies weekly, and treat every external input as hostile. Everything below gives you the code to back that up.

A01 — Broken Access Control

This one is #1 because it's trivially easy to miss. You check authentication (is the user logged in?) but skip authorization (can this user access this resource?).

IDOR Simulator — GET /api/orders/:id

No ownership check — any authenticated user can access any order by ID

Logged in as

Request order

Server response

← click an order to send a request
db.query("SELECT * FROM orders WHERE id = $1", [orderId])

The classic IDOR (Insecure Direct Object Reference): an endpoint like GET /api/orders/4821 returns order data for whoever asks — as long as they have any valid token.

// ❌ 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 — 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.

A02 — Cryptographic Failures

Previously called "Sensitive Data Exposure," the rename is more accurate. The failure is usually in how you handle cryptography — not just whether you use it.

Storing passwords with MD5 or SHA-1 is the obvious mistake. The less obvious one: encrypting data with AES-ECB mode (which reveals patterns in identical plaintext blocks) or using a static IV with AES-CBC.

const bcrypt = require('bcrypt');

// ❌ Vulnerable — MD5 is not a password hash
const hash = crypto.createHash('md5').update(password).digest('hex');

// ✅ Fixed — bcrypt with a cost factor of 12+
const hash = await bcrypt.hash(password, 12);
const valid = await bcrypt.compare(inputPassword, hash);

A03 — Injection

SQL injection gets all the press, but injection covers command injection, LDAP injection, NoSQL injection (yes, MongoDB is vulnerable), and template injection. The fix is always the same: never concatenate user input into a query or command string.

// ❌ Vulnerable SQL injection
const query = `SELECT * FROM users WHERE email = '${req.body.email}'`;

// ✅ Fixed — parameterized query
const result = await db.query(
'SELECT * FROM users WHERE email = $1',
[req.body.email]
);

// ❌ Vulnerable NoSQL injection (MongoDB)
User.findOne({ username: req.body.username, password: req.body.password });
// Attacker sends: { "password": { "$gt": "" } }

// ✅ Fixed — validate input type before querying
if (typeof req.body.password !== 'string') return res.status(400).end();

A04 — Insecure Design

This one can't be fixed with a code patch — it requires changing how you think about features during design. Classic example: a password reset flow that uses a predictable token (timestamp + user ID), or a "security question" that can be brute-forced.

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.

A05 — Security Misconfiguration

Misconfiguration is the broadest category. It covers default credentials left in place, overly permissive CORS, verbose error messages leaking stack traces, and S3 buckets set to public.

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

A06 — Vulnerable and Outdated Components

Log4Shell (CVE-2021-44228) took down organizations worldwide because a logging library had an unauthenticated RCE. The library was buried four levels deep in a dependency tree nobody was actively auditing.

Run npm audit, pip-audit, or OWASP Dependency-Check in your CI pipeline. Set it to fail the build on high-severity findings. This is not optional.

A07 — Identification and Authentication Failures

Weak session tokens, missing rate limits on login endpoints, and accepting JWTs signed with alg: none all live here. 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".

Always verify the algorithm explicitly on the server side, never trust the algorithm from the token header.

const jwt = require('jsonwebtoken');

// ❌ Vulnerable — trusts the algorithm from the token
const decoded = jwt.decode(token); // doesn't verify signature

// ✅ Fixed — specify algorithm explicitly
const decoded = jwt.verify(token, process.env.JWT_SECRET, {
algorithms: ['HS256'] // reject anything else, including 'none'
});

A08 — Software and Data Integrity Failures

This covers CI/CD pipeline compromises and deserialization attacks. 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.

For deserialization: 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.

A09 — Security Logging and Monitoring Failures

The average time to detect a breach is still measured in months. That's almost always a logging failure. Log authentication events (success and failure), access control decisions, input validation failures, and any privilege escalation.

What you should not log: passwords, full credit card numbers, session tokens, PII in URLs. Logging a URL like /reset?token=abc123 leaks the reset token into your log aggregator.

A10 — Server-Side Request Forgery (SSRF)

SSRF moved into the Top 10 because cloud infrastructure made it catastrophic. An attacker tricks your server into making a request to http://169.254.169.254/latest/meta-data/ — the AWS instance metadata endpoint — and can retrieve IAM credentials with a single curl command.

const { URL } = require('url');

function isSafeUrl(input) {
try {
  const url = new URL(input);
  // Block private/internal ranges and metadata endpoints
  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
if (!isSafeUrl(req.body.webhookUrl)) {
return res.status(400).json({ error: 'Invalid URL' });
}

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.


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.

Share this post

Practice exploiting and defending OWASP vulnerabilities

Free, browser-based — no signup required.

Frequently Asked Questions

Related posts