// LOADING
// LOADING
// LOADING_ARTICLE
The OWASP Top 10 is the closest thing the web security community has to a canonical list of what actually gets applications breached. Reading it in the abstract is not that useful. Reading it through the lens of a JavaScript/Node.js stack—Express routes, MongoDB documents, React frontends, JWT auth—is.
This post maps each category to the patterns you encounter day to day. For a broader threat landscape, see Common Web Security Threats and How to Prevent Them.
The number-one risk. Access control breaks when the server trusts the client to identify what it is allowed to touch.
Common JS patterns:
req.body.userId from the client instead of req.user.id from the verified token.ts// Vulnerable: trusts client-supplied userId app.get("/api/orders/:id", async (req, res) => { const order = await Order.findById(req.params.id); res.json(order); // returns any user's order }); // Fixed: verifies ownership against the authenticated token app.get("/api/orders/:id", authenticate, async (req, res) => { const order = await Order.findOne({ _id: req.params.id, userId: req.user.id, // ownership check }); if (!order) return res.status(404).json({ error: "Not found" }); res.json(order); });
For JWT authentication specifics, see How Modern Authentication Works.
Previously called "Sensitive Data Exposure." This covers transmitting or storing data without adequate protection.
localStorage. See Why You Should Never Store JWT in LocalStorage.Strict-Transport-Security.Injection means passing attacker-controlled data to an interpreter—SQL, NoSQL, OS command, or LDAP—in a way that changes the query's meaning.
MongoDB NoSQL injection is underappreciated in the JS world:
ts// Vulnerable: operator injection const user = await User.findOne({ username: req.body.username }); // If req.body.username = { $ne: null }, this returns the first user in the DB // Fixed: validate input type first const { username } = CreateUserSchema.parse(req.body); // z.string() const user = await User.findOne({ username });
Validating types before they reach the database eliminates operator injection entirely. See Runtime Validation in TypeScript with Zod.
Insecure design is when the threat model was never considered, not just when a control was implemented poorly. Rate-limit your login endpoint. Implement account lockout. Design password-reset flows so they don't leak whether an email exists.
DEBUG=true in production, exposing stack traces in API responses.X-Powered-By: Express) that fingerprint your stack.Access-Control-Allow-Origin: *) on authenticated endpoints..env files or admin routes without authentication.For secrets management, see Environment Variables and Secrets Management in Next.js.
Your node_modules tree is a large, shifting dependency surface. Run npm audit regularly and keep dependencies current. Pay particular attention to transitive dependencies—the left-pad and event-stream incidents both came from transitive packages, not direct dependencies.
Automate dependency updates with tools like Renovate or Dependabot in CI. See CI/CD for Next.js with GitHub Actions for pipeline setup.
/login allows credential stuffing.For implementing rate limiting, see Rate Limiting Your Next.js APIs.
This covers insecure deserialization and CI/CD pipeline attacks. In the JS world:
eval(), new Function(), and JSON.parse() on untrusted strings without prior validation.child_process.exec() and shell interpolation.html<script src="https://cdn.example.com/lib.min.js" integrity="sha384-abc123..." crossorigin="anonymous" ></script>
Most breaches are discovered weeks or months after they begin, often by a third party. The countermeasure is structured logging with alerting on anomalous patterns.
Log: authentication failures, authorization denials, input validation errors, rate-limit hits, admin actions. Include: timestamp, user ID (if available), IP, endpoint, and a short reason string. Never log passwords, tokens, or PII.
SSRF occurs when your server makes an HTTP request to a URL supplied by the user, allowing an attacker to reach internal services (metadata endpoints, Redis, internal APIs) that are not exposed externally.
ts// Vulnerable app.post("/fetch-preview", async (req, res) => { const html = await fetch(req.body.url).then(r => r.text()); res.send(html); }); // Mitigated: parse and allowlist import { URL } from "url"; const ALLOWED_HOSTS = new Set(["api.example.com", "cdn.example.com"]); app.post("/fetch-preview", async (req, res) => { const parsed = new URL(req.body.url); // throws on invalid URL if (!ALLOWED_HOSTS.has(parsed.hostname)) { return res.status(400).json({ error: "Host not allowed" }); } const html = await fetch(parsed.href).then(r => r.text()); res.send(html); });
The OWASP Top 10 is not a checklist to complete once and forget. It describes recurring architectural patterns that reappear in every generation of technology. For JavaScript developers specifically:
For deep dives on individual issues from this list, see Preventing XSS in React and Next.js, CSRF Protection in Modern Web Apps, and Securing REST APIs: Best Practices. If you want to discuss your specific security architecture, reach out via the contact page.