The OWASP Top 10 is the most authoritative list of security risks for web applications. The 2025 edition brings important shifts: broken access control remains #1, but server-side request forgery and software supply chain risks have risen significantly. In this comprehensive article we cover all ten categories with practical examples and solutions.
A01: Broken Access Control
Broken Access Control has been the most common vulnerability for years. It comes down to failing to enforce that users can only do what they are authorized for. Think IDOR (Insecure Direct Object References), privilege escalation and bypassing access controls.
The solution starts with a robust authorization framework: use role-based or attribute-based access control (RBAC/ABAC), validate every request server-side, and never rely on client-side checks alone. Deny by default: if a user has no explicit permission, access is denied.
// Example: IDOR vulnerability
// WRONG - user can query any order
app.get('/api/orders/:id', auth, async (req, res) => {
const order = await Order.findById(req.params.id);
res.json(order); // No check if order belongs to user!
});
// CORRECT - add authorization check
app.get('/api/orders/:id', auth, async (req, res) => {
const order = await Order.findOne({
_id: req.params.id,
userId: req.user.id // Only own orders
});
if (!order) return res.status(404).json({ error: 'Not found' });
res.json(order);
});
A02: Cryptographic Failures
Cryptographic Failures (formerly "Sensitive Data Exposure") is about failing to protect data in transit and at rest. Think passwords stored in plain text, use of outdated hash algorithms like MD5 or SHA1, missing TLS, or hardcoded encryption keys in source code.
Always use strong algorithms: bcrypt or Argon2 for passwords, AES-256-GCM for encryption, TLS 1.3 for data in transit. Never store sensitive data unnecessarily and classify your data to determine what protection is needed.
// WRONG - storing password with MD5
const hash = crypto.createHash('md5').update(password).digest('hex');
// CORRECT - use bcrypt with salt rounds
const bcrypt = require('bcrypt');
const saltRounds = 12;
const hash = await bcrypt.hash(password, saltRounds);
// Verification
const isValid = await bcrypt.compare(inputPassword, storedHash);
A03: Injection
Injection attacks occur when untrusted data is sent to an interpreter as part of a command or query. SQL injection is the most well-known, but NoSQL injection, OS command injection and LDAP injection also fall into this category.
Always use parameterized queries or prepared statements. Use ORMs that automatically apply escaping. Validate and sanitize all input server-side. For OS commands: avoid exec() and use specific library functions instead.
// WRONG - SQL injection vulnerability
const query = `SELECT * FROM users WHERE id = ${req.params.id}`;
db.query(query);
// CORRECT - parameterized query
const query = 'SELECT * FROM users WHERE id = ?';
db.query(query, [req.params.id]);
// ORM example (Sequelize)
const user = await User.findOne({
where: { id: req.params.id }
});
A04: Insecure Design
Insecure Design is a broad concept pointing to fundamental design flaws. The difference from implementation bugs is important: a perfectly implemented insecure architecture is still insecure. Think of a password-reset flow using security questions (answers are often guessable), or an e-commerce app without rate limiting on coupon codes.
The solution: use threat modeling early in the development process. Implement secure design patterns like defense-in-depth, least privilege and fail-safe defaults. Test with abuse cases, not just happy paths.
// WRONG - no rate limiting on password reset
app.post('/api/reset-password', async (req, res) => {
const user = await User.findByEmail(req.body.email);
if (user) sendResetEmail(user); // No limit!
res.json({ message: 'Check your email' });
});
// CORRECT - rate limiting + account lockout
const rateLimit = require('express-rate-limit');
const resetLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 3, // max 3 attempts
message: 'Too many requests, please try again later'
});
app.post('/api/reset-password', resetLimiter, async (req, res) => {
// ... secure implementation
});
A05: Security Misconfiguration
Security Misconfiguration is the most broadly occurring issue. It encompasses missing security hardening, unnecessary features enabled, default accounts and passwords, overly informative error messages, and misconfigured HTTP security headers.
Automate your server configuration with Infrastructure as Code. Remove unnecessary features, ports and services. Implement security headers (CSP, HSTS, X-Frame-Options). Use minimal permissions and don't run root processes.
# Nginx security headers configuration
server {
# Strict Transport Security
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# Content Security Policy
add_header Content-Security-Policy "default-src 'self'; script-src 'self'" always;
# Prevent clickjacking
add_header X-Frame-Options "SAMEORIGIN" always;
# Prevent MIME type sniffing
add_header X-Content-Type-Options "nosniff" always;
# Hide server version
server_tokens off;
}
A06: Vulnerable & Outdated Components
Modern applications contain dozens to hundreds of dependencies. Each of these can contain vulnerabilities. In 2024, more than 29,000 new CVEs were published. Keeping track of these without tooling is impossible.
Use Software Composition Analysis (SCA) tools like Snyk, npm audit or Dependabot. Remove unused dependencies. Pin your versions and update regularly. Monitor for new CVEs that affect your stack and have a patch strategy ready.
# Check vulnerabilities in your project
npm audit
# Automatically fix vulnerable packages
npm audit fix
# Snyk scan for deeper analysis
npx snyk test
# Show outdated packages
npm outdated
# Dependabot config (.github/dependabot.yml)
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
A07: Identification & Authentication Failures
Authentication failures include brute-force attacks, credential stuffing, weak password policies, missing multi-factor authentication (MFA) and poor session management. Session fixation, session hijacking and not invalidating sessions on logout are common mistakes.
Implement MFA where possible, use strong password policies (minimum 8 characters, check against leaked password databases), and use proven session management frameworks. Limit login attempts and implement account lockout mechanisms.
// Login with brute-force protection
const loginAttempts = new Map();
app.post('/api/login', async (req, res) => {
const { email, password } = req.body;
const attempts = loginAttempts.get(email) || { count: 0, lockUntil: 0 };
// Check lockout
if (attempts.lockUntil > Date.now()) {
return res.status(429).json({ error: 'Account temporarily locked' });
}
const user = await User.findByEmail(email);
const valid = user && await bcrypt.compare(password, user.password);
if (!valid) {
attempts.count++;
if (attempts.count >= 5) {
attempts.lockUntil = Date.now() + 15 * 60 * 1000; // 15 min lock
}
loginAttempts.set(email, attempts);
return res.status(401).json({ error: 'Invalid credentials' });
}
loginAttempts.delete(email);
const token = generateJWT(user);
res.json({ token });
});
A08: Software & Data Integrity Failures
This category focuses on assumptions about software updates, critical data and CI/CD pipelines without integrity verification. Think of the SolarWinds attack: attackers compromised the build system and added malware to legitimate software updates that were installed by thousands of organizations.
Use Subresource Integrity (SRI) for external scripts and stylesheets. Digitally sign software releases. Secure your CI/CD pipeline with code signing, pipeline-as-code with code reviews, and separated build environments.
// Subresource Integrity (SRI) for external scripts
<script
src="https://cdn.example.com/lib.min.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxAh6VgnSY"
crossorigin="anonymous">
</script>
// Lock file verification in CI pipeline
// Always commit package-lock.json or yarn.lock
// Use npm ci instead of npm install in CI
npm ci --ignore-scripts // Prevent post-install scripts
// Verify package integrity
npm audit signatures
A09: Security Logging & Monitoring Failures
Without adequate logging and monitoring, attacks are only discovered when it's too late. Research shows the average time to detect a breach is over 200 days. Many organizations log insufficient events, don't retain logs long enough, or don't actively monitor them.
Log all authentication events (login, logout, failed attempts), access control failures, input validation failures and server-side errors. Centralize logs with tools like ELK Stack or Grafana Loki. Set up alerting for suspicious patterns and regularly test your incident response plan.
// Structured security logging with Winston
const winston = require('winston');
const securityLogger = winston.createLogger({
level: 'info',
format: winston.format.json(),
defaultMeta: { service: 'auth-service' },
transports: [
new winston.transports.File({ filename: 'security.log' })
]
});
// Log failed login attempts
app.post('/api/login', async (req, res) => {
const { email } = req.body;
const success = await authenticate(email, req.body.password);
securityLogger.info({
event: success ? 'LOGIN_SUCCESS' : 'LOGIN_FAILURE',
email,
ip: req.ip,
userAgent: req.headers['user-agent'],
timestamp: new Date().toISOString()
});
});
A10: Server-Side Request Forgery (SSRF)
SSRF attacks trick the server into making requests to unintended locations. In cloud environments this is extra dangerous: attackers can access the metadata endpoint (e.g. http://169.254.169.254 on AWS) to steal credentials. The 2019 Capital One breach was an SSRF attack that leaked 100 million records.
Validate and sanitize all user-supplied URLs. Use an allowlist of permitted domains and protocols. Block requests to private IP ranges and cloud metadata endpoints. Use network-level segmentation to limit the blast radius.
// WRONG - user-supplied URL without validation
app.post('/api/fetch-url', async (req, res) => {
const response = await fetch(req.body.url); // SSRF!
res.json(await response.json());
});
// CORRECT - URL validation with allowlist
const allowedDomains = ['api.example.com', 'cdn.example.com'];
function isAllowedUrl(urlString) {
try {
const url = new URL(urlString);
if (!['http:', 'https:'].includes(url.protocol)) return false;
if (!allowedDomains.includes(url.hostname)) return false;
// Block private IP ranges
const ip = url.hostname;
if (ip.startsWith('10.') || ip.startsWith('192.168.') ||
ip.startsWith('169.254.') || ip === 'localhost') return false;
return true;
} catch { return false; }
}
app.post('/api/fetch-url', async (req, res) => {
if (!isAllowedUrl(req.body.url)) {
return res.status(400).json({ error: 'URL not allowed' });
}
const response = await fetch(req.body.url);
res.json(await response.json());
});
Practical Security Strategy
Use the OWASP Top 10 as a starting point for your security roadmap, not as the destination. Start with a threat model for your application: what data is most valuable? Who are potential attackers? Which attack vectors are most likely?
Integrate security testing into your CI/CD pipeline with tools like OWASP ZAP (DAST), Semgrep (SAST) and Snyk (SCA). Automate what you can, and schedule regular penetration tests for what you cannot automate.
Pro Tip
Run an automated security scan after every deployment. Tools like OWASP ZAP can run as a step in your CI/CD pipeline and automatically report vulnerabilities before code reaches production.
Share this article