The OWASP Top 10: Your App's Security Report Card 📊
The OWASP Top 10: Your App's Security Report Card 📊
So you built an app. Congrats! Now let me tell you how hackers are going to break it. 🎉
The OWASP Top 10 is basically the "Greatest Hits" album of web vulnerabilities - the most common ways developers accidentally create security disasters. As someone who's spent years building production systems and lurking in security communities, let me translate this from "security consultant speak" into "normal human language."
Think of this as your app's physical exam. Except instead of checking your cholesterol, we're checking if your login form is basically a welcome mat for hackers.
What Even Is OWASP? 🤔
OWASP = Open Web Application Security Project. They're basically the security nerds who track how apps get hacked, then publish a "Top 10 Ways You're Probably Screwing Up" list every few years.
Fun fact: The list changes over time because we keep inventing new ways to mess up! The 2021 version (latest as of my knowledge) includes some "classics" and some new entries that made me go "Oh no, I've definitely done that."
Let's speedrun the list with ACTUAL examples (not boring theory).
#1: Broken Access Control 🚪
Translation: Users accessing stuff they shouldn't.
Real-world example:
// Your innocent-looking API endpoint
app.get('/api/orders/:orderId', async (req, res) => {
const order = await Order.findById(req.params.orderId);
return res.json(order); // Uh oh... 😬
});
The attack:
# My order
GET /api/orders/12345
# Hey, what if I just... try another number?
GET /api/orders/12346 # Returns someone else's order! 🎁
The fix:
// Check if the user OWNS this order!
app.get('/api/orders/:orderId', async (req, res) => {
const order = await Order.findById(req.params.orderId);
// Actually verify ownership
if (!order || order.userId !== req.user.id) {
return res.status(403).json({ error: 'Access denied' });
}
return res.json(order);
});
Pro Tip: In my experience building e-commerce backends, this is THE most common vulnerability I see in code reviews. Every. Single. Time.
#2: Cryptographic Failures 🔐
Translation: You're encrypting things wrong (or not at all).
Classic mistake:
// Storing passwords like it's 1999
await db.users.insert({
username: 'alice',
password: 'hunter2' // PLEASE NO 😱
});
// Slightly less terrible but still bad
await db.users.insert({
username: 'bob',
password: md5('password123') // MD5 in 2026?!
});
The right way:
const bcrypt = require('bcrypt');
// Hash with proper salting and rounds
const hashedPassword = await bcrypt.hash(password, 12);
await db.users.insert({
username: 'alice',
password: hashedPassword // Now we're talking! ✨
});
// Verification
const isValid = await bcrypt.compare(inputPassword, storedHash);
Real Talk: I once saw a production Laravel app storing passwords in plain text because the developer "didn't want users to forget them." I still have nightmares.
#3: Injection 💉
Translation: Your app executes user input as code. Very bad.
SQL Injection:
// How to get hacked in one easy step!
$userId = $_GET['id'];
$query = "SELECT * FROM users WHERE id = $userId";
// User sends: ?id=1 OR 1=1
// Query becomes: SELECT * FROM users WHERE id = 1 OR 1=1
// Returns ALL users! 🚨
The fix:
// Use parameterized queries (Laravel)
$user = DB::table('users')
->where('id', $request->id)
->first();
// Or with Eloquent (even better)
$user = User::find($request->id);
NoSQL Injection (yes, it's a thing!):
// MongoDB can be vulnerable too!
const user = await User.findOne({
username: req.body.username,
password: req.body.password // If this is an object... 💀
});
// Attacker sends:
{
"username": "admin",
"password": { "$ne": null } // Means "not equal to null"
}
// Query: find user where username="admin" AND password != null
// Bypasses authentication! 🎭
The fix:
// Sanitize and validate!
const user = await User.findOne({
username: String(req.body.username),
password: String(req.body.password)
});
// Better: use proper authentication libraries
// bcrypt.compare() does this for you!
Personal story: As someone passionate about security, I've seen injection attacks in CTF competitions that were so creative they made me laugh and cry simultaneously.
#4: Insecure Design 🏗️
Translation: Your whole approach is flawed. You can't patch your way out of bad architecture.
Example: Password recovery by "security questions"
// INSECURE BY DESIGN
app.post('/reset-password', async (req, res) => {
const user = await User.findOne({ email: req.body.email });
// "What's your mother's maiden name?"
// Spoiler: It's on Facebook 🤦♂️
if (req.body.securityAnswer === user.securityAnswer) {
// Reset password
}
});
Better design:
// Use time-limited, cryptographically secure tokens
const crypto = require('crypto');
app.post('/forgot-password', async (req, res) => {
const user = await User.findOne({ email: req.body.email });
// Generate secure random token
const resetToken = crypto.randomBytes(32).toString('hex');
const resetExpiry = Date.now() + 3600000; // 1 hour
await user.update({ resetToken, resetExpiry });
// Email the token link
sendEmail(user.email, `Reset link: /reset/${resetToken}`);
});
Lesson: Sometimes the feature itself is the vulnerability. Think threat modeling BEFORE you code.
#5: Security Misconfiguration ⚙️
Translation: Leaving default settings, exposing debug info, basically being lazy.
Common fails:
// Exposed error details in production
app.get('/api/data', async (req, res) => {
try {
const data = await fetchSensitiveData();
res.json(data);
} catch (error) {
// DON'T DO THIS!
res.status(500).json({
error: error.message,
stack: error.stack, // Full stack trace! 📚
dbConnection: process.env.DB_HOST // Credentials leak!
});
}
});
The fix:
// Production-safe error handling
app.get('/api/data', async (req, res) => {
try {
const data = await fetchSensitiveData();
res.json(data);
} catch (error) {
// Log internally
logger.error('Data fetch failed', { error, userId: req.user.id });
// Return generic message
res.status(500).json({ error: 'Internal server error' });
}
});
In security communities, we often discuss: How many data breaches started with a misconfigured S3 bucket? (Answer: Too many! 🪣)
#6: Vulnerable and Outdated Components 📦
Translation: Using old, hacked libraries.
The nightmare:
# Your package.json from 2019
npm audit
# 47 vulnerabilities (23 critical) 😱
The fix:
# Update dependencies regularly!
npm audit fix
# Check for outdated packages
npm outdated
# Use tools like Dependabot or Renovate
# Let robots keep your dependencies fresh 🤖
Pro Tip: I've seen production Laravel apps running on PHP 5.6 (released in 2014!) because "if it works, don't touch it." Then it gets hacked. Then they touch it.
#7: Identification and Authentication Failures 🎭
Translation: Your login is broken.
Bad auth patterns:
// Weak password requirements
function isValidPassword(password) {
return password.length >= 6; // Hunter2 is valid! 🙈
}
// No rate limiting
app.post('/login', async (req, res) => {
// Hacker tries 1 million passwords
// You: "This is fine" 🔥
});
// Session fixation vulnerability
app.post('/login', async (req, res) => {
if (validCredentials) {
req.session.userId = user.id; // Reuses old session ID!
}
});
Better approach:
// Strong password requirements
function isValidPassword(password) {
return password.length >= 12 &&
/[A-Z]/.test(password) &&
/[a-z]/.test(password) &&
/[0-9]/.test(password) &&
/[^A-Za-z0-9]/.test(password);
}
// Rate limiting (using express-rate-limit)
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 attempts
message: 'Too many login attempts, try again later'
});
app.post('/login', loginLimiter, async (req, res) => {
if (validCredentials) {
// Regenerate session ID after login!
req.session.regenerate(() => {
req.session.userId = user.id;
});
}
});
#8: Software and Data Integrity Failures 🔄
Translation: Not verifying that code/data hasn't been tampered with.
Risky pattern:
// Loading scripts from CDN without verification
<script src="https://cdn.example.com/library.js"></script>
// What if the CDN gets hacked? 🤔
Safer:
<!-- Use Subresource Integrity (SRI) -->
<script
src="https://cdn.example.com/library.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
crossorigin="anonymous">
</script>
<!-- Browser verifies the hash! ✅ -->
In CI/CD:
# Verify dependencies before deployment
- name: Verify checksums
run: |
npm ci # Uses package-lock.json for exact versions
npm audit signatures # Verify package signatures
#9: Security Logging and Monitoring Failures 📊
Translation: Getting hacked and not even noticing.
What NOT to do:
// Silent failures
app.post('/login', async (req, res) => {
if (!validCredentials) {
return res.status(401).send('Invalid credentials');
// No logging! 🙈
}
});
What you SHOULD do:
const logger = require('winston');
app.post('/login', async (req, res) => {
if (!validCredentials) {
// Log failed attempts!
logger.warn('Failed login attempt', {
username: req.body.username,
ip: req.ip,
userAgent: req.headers['user-agent'],
timestamp: new Date()
});
return res.status(401).send('Invalid credentials');
}
// Log successful logins too
logger.info('Successful login', {
userId: user.id,
ip: req.ip
});
});
// Set up alerts for suspicious patterns
// 10 failed logins from same IP? Send alert! 🚨
Real Talk: In production systems, I've set up CloudWatch alarms that ping Slack when we see unusual patterns. Caught a credential stuffing attack in progress once - super satisfying! 🛡️
#10: Server-Side Request Forgery (SSRF) 🌐
Translation: Making YOUR server attack other servers.
Vulnerable code:
// User-controlled URL fetch (danger!)
app.get('/fetch-image', async (req, res) => {
const imageUrl = req.query.url;
const response = await fetch(imageUrl); // 💣
// User sends: ?url=http://169.254.169.254/latest/meta-data/
// Your server fetches AWS metadata! Credentials leaked! 😱
});
The fix:
const url = require('url');
app.get('/fetch-image', async (req, res) => {
const imageUrl = req.query.url;
const parsed = url.parse(imageUrl);
// Whitelist allowed domains
const allowedHosts = ['cdn.example.com', 'images.example.com'];
if (!allowedHosts.includes(parsed.host)) {
return res.status(400).json({ error: 'Invalid URL' });
}
// Block internal IP ranges
const blockedRanges = ['127.0.0.1', '169.254.169.254', 'localhost'];
if (blockedRanges.some(range => imageUrl.includes(range))) {
return res.status(400).json({ error: 'Forbidden URL' });
}
const response = await fetch(imageUrl);
// Now we're safe(r)! ✅
});
Your OWASP Top 10 Checklist 📋
Before you ship to production, check these:
- Access Control: Every endpoint checks user permissions
- Crypto: Passwords use bcrypt/argon2 (not MD5!)
- Injection: Using parameterized queries everywhere
- Design: Threat modeling done, security requirements defined
- Config: No default passwords, debug mode off in production
- Dependencies:
npm auditshows 0 critical issues - Auth: Rate limiting on login, MFA available
- Integrity: Using SRI for external scripts
- Logging: Failed logins and suspicious activity logged
- SSRF: User-provided URLs validated and restricted
Quick Wins (Do These NOW!) 🏃♂️
- Run
npm auditand fix critical issues - Add rate limiting to login endpoints
- Check if you're logging failed authentication attempts
- Review any endpoint that takes an ID - does it check ownership?
- Scan your code for string concatenation in SQL queries
Tools That'll Save Your Butt 🛠️
- OWASP ZAP: Free security scanner (actual pentester in a box!)
- Snyk: Finds vulnerabilities in dependencies
- SonarQube: Static code analysis with security rules
- npm audit / yarn audit: Built-in dependency checking
- Helmet.js: Security headers for Express apps
The Bottom Line
The OWASP Top 10 isn't meant to scare you (okay, maybe a little 😅). It's a prioritized list of "fix these first" security issues.
Good news: You don't need to be a security expert to avoid most of these! Just:
- Validate inputs (never trust user data)
- Use libraries correctly (read the docs!)
- Keep dependencies updated (robots can help!)
- Think like an attacker ("What if I change this URL parameter?")
- Log everything suspicious (you'll thank yourself later)
Security doesn't have to be overwhelming. Start with the Top 10, fix what you can, and iterate. Your future self (and your users) will thank you!
Want to dive deeper? Check out the official OWASP Top 10 or connect with me on LinkedIn to discuss security! 🔐
Building secure systems? Drop your war stories on GitHub - I love learning from other people's close calls!
P.S. - If you just realized your app has all 10 vulnerabilities, don't panic. Prioritize, fix the critical stuff first, and remember: everyone's code is broken until it's tested by hackers. 🛡️✨