OWASP Top 10 (2025): Complete Web Security Threats Overview
Prologue: Security Is Not an Afterthought
"Features first, security later." It's a natural thought during development. But security vulnerabilities surface in one of two ways: during code review, or after getting breached.
OWASP (Open Worldwide Application Security Project) regularly publishes the 10 most critical web application security risks. Knowing this list gives you a checklist for "where do I need to be careful?"
Let's go through the 2025 OWASP Top 10 one by one with real code.
A01: Broken Access Control
Number one since 2021. Most common. Most critical.
Vulnerable Code
// Vulnerable — no ownership check on the resource
app.get("/api/users/:id/profile", async (req, res) => {
const { id } = req.params;
// No check if the logged-in user is requesting their own profile
const user = await db.users.findById(id);
res.json(user);
});
// Attacker: GET /api/users/9999/profile → other user's data
This is IDOR (Insecure Direct Object Reference).
Safe Code
app.get("/api/users/:id/profile", authenticate, async (req, res) => {
const requestedId = req.params.id;
const currentUserId = req.user.id;
if (requestedId !== currentUserId && req.user.role !== "admin") {
return res.status(403).json({ error: "Forbidden" });
}
const user = await db.users.findById(requestedId);
res.json(user);
});
Core rule: Always check "does this person have permission to access this resource?" on the server.
A02: Cryptographic Failures
Previously called "Sensitive Data Exposure." Covers missing encryption, weak algorithms, or flawed implementations.
Vulnerable
// Storing plaintext passwords
await db.users.create({ email, password: "mypassword123" });
// Weak hash
const hash = crypto.createHash("md5").update(password).digest("hex");
// Logging sensitive data
console.log(`Login: ${user.email}, password: ${password}`);
Safe
import bcrypt from "bcrypt";
const hashedPassword = await bcrypt.hash(password, 12);
await db.users.create({ email, password: hashedPassword });
const isValid = await bcrypt.compare(inputPassword, storedHash);
// Strip sensitive fields from responses
const { password: _, ...safeUser } = user;
res.json(safeUser);
A03: Injection
SQL injection is the classic, but also covers OS command, LDAP, and NoSQL injection.
SQL Injection
// Vulnerable — string concatenation
const query = `SELECT * FROM users WHERE email = '${email}' AND password = '${password}'`;
// Attack: email = "admin' --"
// Generated: SELECT * FROM users WHERE email = 'admin' --' AND password = '...'
// Password check commented out!
// Safe — prepared statements
const result = await db.query(
"SELECT * FROM users WHERE email = $1 AND password = $2",
[email, password]
);
NoSQL Injection
// MongoDB vulnerable
const user = await User.findOne({ email, password });
// Attack: { "password": { "$ne": null } } → matches any user!
// Safe — type validation
const loginSchema = z.object({
email: z.string().email(),
password: z.string().min(8).max(100),
// Only strings accepted, no operator objects
});
app.post("/login", async (req, res) => {
const { email, password } = loginSchema.parse(req.body);
const user = await User.findOne({ email });
if (!user || !await bcrypt.compare(password, user.password)) {
return res.status(401).json({ error: "Invalid credentials" });
}
});
A04: Insecure Design
Not a code-level bug — security not considered during the design phase.
// Safe password reset design: token-based
app.post("/forgot-password", async (req, res) => {
const { email } = req.body;
const user = await User.findOne({ email });
// Don't reveal if email exists (prevents user enumeration)
if (!user) {
return res.json({ message: "If this email exists, a reset link was sent." });
}
const token = crypto.randomBytes(32).toString("hex");
const expiresAt = new Date(Date.now() + 1000 * 60 * 15); // 15 min
await PasswordResetToken.create({ userId: user.id, token, expiresAt });
await sendEmail(email, `Reset: https://app.com/reset?token=${token}`);
res.json({ message: "If this email exists, a reset link was sent." });
});
app.post("/reset-password", async (req, res) => {
const { token, newPassword } = req.body;
const resetToken = await PasswordResetToken.findOne({
token,
expiresAt: { $gt: new Date() },
used: false, // prevent reuse
});
if (!resetToken) {
return res.status(400).json({ error: "Invalid or expired token" });
}
await User.update(resetToken.userId, {
password: await bcrypt.hash(newPassword, 12),
});
await resetToken.update({ used: true });
res.json({ message: "Password updated" });
});
A05: Security Misconfiguration
Default configs, unnecessary features enabled, overly verbose error messages.
// Vulnerable
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
res.status(500).json({
error: err.message,
stack: err.stack, // Never expose stack traces
query: req.query, // Never expose request internals
});
});
// Safe
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
const isDev = process.env.NODE_ENV === "development";
console.error(err);
res.status(500).json({
error: isDev ? err.message : "Internal Server Error",
});
});
// Use Helmet for security headers
import helmet from "helmet";
app.use(helmet());
// Sets X-Frame-Options, X-Content-Type-Options, HSTS, etc.
// CORS whitelist
const allowedOrigins = ["https://myapp.com"];
app.use(cors({
origin: (origin, callback) => {
if (!origin || allowedOrigins.includes(origin)) callback(null, true);
else callback(new Error("Not allowed by CORS"));
},
}));
A06: Vulnerable and Outdated Components
npm audit
# found 3 vulnerabilities (1 moderate, 2 high)
npm audit fix
npm update axios
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
A07: Identification and Authentication Failures
import rateLimit from "express-rate-limit";
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
message: "Too many login attempts, please try again later",
});
app.post("/login", loginLimiter, async (req, res) => {
const { email, password } = loginSchema.parse(req.body);
const user = await User.findOne({ email });
const isValid = user && await bcrypt.compare(password, user.password);
if (!isValid) return res.status(401).json({ error: "Invalid credentials" });
const accessToken = jwt.sign(
{ userId: user.id, role: user.role },
process.env.JWT_SECRET!,
{ expiresIn: "15m" }
);
// Store refresh token in httpOnly cookie (prevents XSS theft)
res.cookie("refreshToken", refreshToken, {
httpOnly: true,
secure: true,
sameSite: "strict",
});
res.json({ accessToken });
});
A09: Security Logging and Monitoring Failures
const securityLogger = winston.createLogger({
level: "info",
format: winston.format.json(),
transports: [new winston.transports.File({ filename: "security.log" })],
});
// Log login successes and failures for anomaly detection
securityLogger.info("login_success", { userId: user.id, ip, timestamp: new Date() });
securityLogger.warn("login_failed", { email, ip, reason: "invalid_credentials", timestamp: new Date() });
A10: Server-Side Request Forgery (SSRF)
// Vulnerable — fetching user-supplied URLs
app.post("/api/fetch-url", async (req, res) => {
const { url } = req.body;
// Attacker can send: http://169.254.169.254/latest/meta-data/ (AWS metadata)
const response = await fetch(url);
res.json(await response.json());
});
// Safe — whitelist allowed hosts
const ALLOWED_HOSTS = ["api.trusted-service.com"];
function isUrlSafe(urlString: string): boolean {
try {
const url = new URL(urlString);
if (url.protocol !== "https:") return false;
if (!ALLOWED_HOSTS.includes(url.hostname)) return false;
return true;
} catch {
return false;
}
}
app.post("/api/fetch-url", async (req, res) => {
if (!isUrlSafe(req.body.url)) {
return res.status(400).json({ error: "URL not allowed" });
}
const response = await fetch(req.body.url, { signal: AbortSignal.timeout(5000) });
res.json(await response.json());
});
Summary Checklist
| # | Vulnerability | Key Mitigation |
|---|---|---|
| A01 | Broken Access Control | Always verify permissions server-side |
| A02 | Cryptographic Failures | bcrypt, AES-256, HTTPS everywhere |
| A03 | Injection | Prepared statements, input validation |
| A04 | Insecure Design | Threat modeling, secure design patterns |
| A05 | Misconfiguration | Least privilege, Helmet, hide error details |
| A06 | Vulnerable Components | npm audit, Dependabot |
| A07 | Auth Failures | Rate limiting, strong JWT, MFA |
| A08 | Integrity Failures | npm ci, SRI, secure CI/CD |
| A09 | Logging Failures | Log security events, set up alerts |
| A10 | SSRF | URL whitelist, block internal IP ranges |
Takeaway
OWASP Top 10 isn't "this is all you need to secure." It's "these are the minimum you must address." Add each item to your code review checklist. A01 (access control), A03 (injection), and A07 (authentication) are the most frequent and most damaging.
Security isn't a feature — it's a habit.