CalcuOnline
HomeTech Tutorials / ReviewsDeveloper ToolsHow to Audit and Fix OWASP Top 10 Security Vulnerabilities in a Node.js Enterprise API
How to Audit and Fix OWASP Top 10 Security Vulnerabilities in a Node.js Enterprise API

How to Audit and Fix OWASP Top 10 Security Vulnerabilities in a Node.js Enterprise API

Developer Tools 5.0 Updated 24 August 2026

How to use this tutorial

This walks the OWASP Top 10 (2021 edition) categories that most commonly appear in a Node.js/Express enterprise API, in the order you should actually check them during an audit — starting with access control and injection, which tend to be both the most common and the most severe. Each section shows the vulnerable pattern, the fix, and how to verify the fix with an automated check.

1. Technical architecture (audit workflow)

┌─────────────────┐   ┌──────────────────────┐   ┌────────────────────────┐
│ Static analysis      │──▶│ Dependency audit         │──▶│ Manual/dynamic review     │
│ (ESLint security       │   │ (npm audit, Snyk,          │   │ (auth flows, SSRF,          │
│  plugin, Semgrep)        │   │  OSV-Scanner)               │   │  business logic)             │
└─────────────────┘   └──────────────────────┘   └────────────┬───────────┘
                                                                    ▼
                                                       ┌────────────────────────┐
                                                       │ Findings list, mapped to    │
                                                       │ OWASP Top 10 categories      │
                                                       │ with severity + CWE ID        │
                                                       └────────────┬───────────┘
                                                                    ▼
                                                       ┌────────────────────────┐
                                                       │ Remediation PRs + regression│
                                                       │ tests (one per finding)       │
                                                       └────────────────────────┘

2. Complete code implementation (findings and fixes)

2.1 A01: Broken Access Control

// VULNERABLE: trusts a client-supplied user id instead of the authenticated session
app.get("/api/orders/:userId", authenticate, async (req, res) => {
  const orders = await db.query("SELECT * FROM orders WHERE user_id = ?", [req.params.userId]);
  res.json(orders);
});
// FIXED: authorization derived from the verified session, never from client input
app.get("/api/orders/:userId", authenticate, async (req, res) => {
  const requestedUserId = req.params.userId;
  const isSelf = req.user.id === requestedUserId;
  const isAdmin = req.user.role === "admin";

  if (!isSelf && !isAdmin) {
    return res.status(403).json({ error: "Forbidden" });
  }

  const orders = await db.query("SELECT * FROM orders WHERE user_id = ?", [requestedUserId]);
  res.json(orders);
});

2.2 A02/A03: Cryptographic failures and Injection

// VULNERABLE: string-concatenated SQL, plaintext password storage
async function createUser(email, password) {
  await db.query(`INSERT INTO users (email, password) VALUES ('${email}', '${password}')`);
}
// FIXED: parameterized query, salted hashing with bcrypt
const bcrypt = require("bcryptjs");

async function createUser(email, password) {
  if (password.length < 12) {
    throw new ValidationError("Password must be at least 12 characters");
  }
  const passwordHash = await bcrypt.hash(password, 12); // cost factor 12
  await db.query("INSERT INTO users (email, password_hash) VALUES (?, ?)", [email, passwordHash]);
}

2.3 A05: Security misconfiguration (headers, CORS)

// FIXED: security headers and a strict, explicit CORS allowlist
const helmet = require("helmet");
const cors = require("cors");

app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'"],
      objectSrc: ["'none'"],
      frameAncestors: ["'none'"],
    },
  },
  hsts: { maxAge: 63072000, includeSubDomains: true, preload: true },
}));

const ALLOWED_ORIGINS = ["https://app.example.com", "https://admin.example.com"];
app.use(cors({
  origin: (origin, callback) => {
    if (!origin || ALLOWED_ORIGINS.includes(origin)) {
      return callback(null, true);
    }
    callback(new Error("Not allowed by CORS"));
  },
  credentials: true,
}));

// Never leak stack traces or internal error detail to clients
app.use((err, req, res, next) => {
  console.error(err); // full detail goes to server-side logs only
  const status = err.status || 500;
  res.status(status).json({ error: status === 500 ? "Internal server error" : err.message });
});

2.4 A08/A10: Insecure deserialization and SSRF

// VULNERABLE: fetches an arbitrary user-supplied URL server-side
app.post("/api/fetch-preview", authenticate, async (req, res) => {
  const response = await fetch(req.body.url); // can hit internal metadata endpoints, private IPs
  const html = await response.text();
  res.json({ preview: html.slice(0, 500) });
});
// FIXED: allowlist scheme/host, block private/link-local ranges, cap redirects
const { URL } = require("url");
const net = require("net");
const dns = require("dns").promises;

const BLOCKED_HOST_PATTERNS = [/^127\./, /^10\./, /^192\.168\./, /^169\.254\./, /^::1$/, /^localhost$/i];

async function isSafeUrl(rawUrl) {
  let parsed;
  try {
    parsed = new URL(rawUrl);
  } catch {
    return false; // not a valid URL at all
  }

  if (!["http:", "https:"].includes(parsed.protocol)) return false;

  const addresses = await dns.resolve4(parsed.hostname).catch(() => []);
  const allTargets = [parsed.hostname, ...addresses];
  for (const target of allTargets) {
    if (BLOCKED_HOST_PATTERNS.some((pattern) => pattern.test(target))) return false;
    if (net.isIP(target) && isPrivateIp(target)) return false;
  }
  return true;
}

function isPrivateIp(ip) {
  const octets = ip.split(".").map(Number);
  if (octets.length !== 4) return false;
  const [a, b] = octets;
  return a === 10 || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168) || a === 169;
}

app.post("/api/fetch-preview", authenticate, async (req, res) => {
  const { url } = req.body;
  if (!(await isSafeUrl(url))) {
    return res.status(400).json({ error: "URL not allowed" });
  }

  try {
    const response = await fetch(url, { redirect: "manual", signal: AbortSignal.timeout(5000) });
    if ([301, 302, 303, 307, 308].includes(response.status)) {
      return res.status(400).json({ error: "Redirects are not followed for preview fetches" });
    }
    const html = await response.text();
    res.json({ preview: html.slice(0, 500) });
  } catch (err) {
    if (err.name === "TimeoutError") {
      return res.status(504).json({ error: "Fetch timed out" });
    }
    res.status(502).json({ error: "Failed to fetch URL" });
  }
});

2.5 A07: Identification and authentication failures — rate limiting

// FIXED: rate limit auth endpoints specifically, separate from general API limits
const rateLimit = require("express-rate-limit");

const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 10,
  standardHeaders: true,
  legacyHeaders: false,
  message: { error: "Too many authentication attempts, please try again later" },
});

app.use("/api/auth/login", authLimiter);
app.use("/api/auth/reset-password", authLimiter);

3. Step-by-step configuration guide

  1. Install audit and remediation tooling:
    npm install helmet cors express-rate-limit bcryptjs
    npm install -D eslint eslint-plugin-security
    
  2. Run dependency and static scans first, before touching code:
    npm audit --production
    npx eslint . --ext .js --no-eslintrc -c .eslintrc.security.json
    
  3. Triage findings by OWASP category and severity — fix Broken Access Control and Injection findings first; they tend to carry the highest real-world exploitation risk in typical enterprise APIs.
  4. Apply the access-control fix pattern (§2.1) to every route that takes a resource ID from the URL/body — grep for req.params and req.body usages that reach a database query without an accompanying ownership/role check.
  5. Replace every string-concatenated query with parameterized queries (§2.2); most ORMs (Prisma, Sequelize, Knex) parameterize by default, so this usually means finding raw db.query/connection.execute calls with template literals.
  6. Add helmet, a strict CORS allowlist, and a generic error handler globally, as early as possible in your middleware chain (§2.3).
  7. Audit every server-side outbound fetch (webhooks, URL previews, image proxies, PDF generators) for SSRF using the isSafeUrl pattern in §2.4 — this is the finding most often missed in manual review because it doesn't look dangerous at a glance.
  8. Add rate limiting to authentication-adjacent endpoints specifically (§2.5), not just a blanket API-wide limiter, since credential-stuffing and password-reset abuse target those routes disproportionately.
  9. Re-run the scans from step 2 and confirm each finding is resolved before closing it out; add a regression test per fixed finding so it can't silently regress in a future PR.

4. Error handling and edge cases

Related Reviews

Building a Production-Ready RAG Pipeline with LangChain, Pinecone, and OpenAI
Building a Production-Ready RAG Pipeline with LangChain, Pinecone, and OpenAI
5.0
A complete retrieval-augmented generation pipeline covering chunking strategy, Pinecone indexing with metadata filters, hybrid retrieval, and a FastAPI se…
Read Review → Developer Tools
Optimizing AI Model Inference Latency in Production Using NVIDIA Triton Inference Server
Optimizing AI Model Inference Latency in Production Using NVIDIA Triton Inference Server
5.0
Deploy a model behind NVIDIA Triton with dynamic batching, concurrent model instances, and ONNX Runtime acceleration, then measure and tune p99 latency un…
Read Review → Developer Tools
Setting Up Automated LLM Evaluation Pipelines for Hallucination Detection Using Ragas
Setting Up Automated LLM Evaluation Pipelines for Hallucination Detection Using Ragas
5.0
Wire Ragas metrics (faithfulness, answer relevancy, context precision/recall) into a CI-runnable evaluation pipeline that scores a RAG system against a go…
Read Review → Developer Tools