
OWASP Top 10 (2025): Complete Web Security Threats Overview
From injection and broken auth to XSS and the newest threats — OWASP Top 10 broken down with real code examples and practical mitigations for each vulnerability.

From injection and broken auth to XSS and the newest threats — OWASP Top 10 broken down with real code examples and practical mitigations for each vulnerability.
A comprehensive deep dive into client-side storage. From Cookies to IndexedDB and the Cache API. We explore security best practices for JWT storage (XSS vs CSRF), performance implications of synchronous APIs, and how to build offline-first applications using Service Workers.

App crashes only in Release mode? It's likely ProGuard/R8. Learn how to debug obfuscated stack traces, use `@Keep` annotations, and analyze `usage.txt`.

Bitcoin is just part of it. How to create trust without a central authority, principles of decentralization, smart contracts, gas fees, Layer 2 solutions, DAOs, and the Oracle Problem.

Why your server isn't hacked. From 'Packet Filtering' checking ports/IPs to AWS Security Groups. Evolution of Firewalls.

"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.
Number one since 2021. Most common. Most critical.
// 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).
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.
Previously called "Sensitive Data Exposure." Covers missing encryption, weak algorithms, or flawed implementations.
// 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}`);
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);
SQL injection is the classic, but also covers OS command, LDAP, and NoSQL 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]
);
// 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" });
}
});
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" });
});
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"));
},
}));
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"
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 });
});
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() });
// 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());
});
| # | 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 |
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.