Coreframe Labs

Automated security audit

Security audits for Node.js & Express APIs

Authentication answers who someone is; almost every real breach we see in Node APIs is a failure of the second question — what that person is allowed to touch. These bugs pass every test, because tests use the ID of the user who owns the record.

Check a file now — free

Paste one file. The free tier runs a single security pass and returns up to three findings, each with the line of code that evidences it. Nothing is stored, nothing is used for training, and detected secrets are redacted before analysis.

Paste one file0 / 400 lines

Not stored. Not trained on. Secrets redacted before analysis.

What we find most often in Node.js & Express APIs

Each of these is a real pattern, with the code that causes it and the change that fixes it.

1. Insecure direct object reference

Symptom
Authenticated endpoint, correct-looking code, and any user can read any record by changing a number in the URL.

The pattern

app.get("/api/invoices/:id", requireAuth, async (req, res) => {
  const invoice = await db.invoice.findUnique({ where: { id: req.params.id } });
  res.json(invoice);
});

Why it bites
`requireAuth` proves the caller is logged in and nothing more. The lookup is keyed entirely on user-supplied input, so invoice 1002 is readable by the owner of invoice 1001. Tests pass because they request the invoice the test user owns.

The fix

app.get("/api/invoices/:id", requireAuth, async (req, res) => {
  // Scope the query by owner so the wrong record can't be addressed at all.
  const invoice = await db.invoice.findFirst({
    where: { id: req.params.id, orgId: req.user.orgId },
  });
  if (!invoice) return res.status(404).end();   // 404, not 403 — don't confirm existence
  res.json(invoice);
});

2. JWT verified without pinning the algorithm

Symptom
Tokens validate correctly. Forged tokens also validate.

The pattern

const payload = jwt.verify(token, process.env.JWT_SECRET);

Why it bites
With no `algorithms` option the library accepts whatever the token's header declares. An attacker re-signs a token as HS256 using your public key as the HMAC secret, or in older libraries sets `alg: none` — and verification succeeds.

The fix

const payload = jwt.verify(token, process.env.JWT_SECRET, {
  algorithms: ["HS256"],       // pin it — never trust the token's own header
  issuer: "coreframe",
  maxAge: "15m",
});

3. Role taken from the request body

Symptom
Privilege escalation via a field the UI never sends.

The pattern

app.post("/api/users", requireAuth, async (req, res) => {
  const user = await db.user.create({ data: req.body });   // body may contain role
  res.json(user);
});

Why it bites
Spreading the request body into a create call means every column is client-controlled. The signup form doesn't render a role field, but the endpoint accepts one, and `{"role":"admin"}` is a one-line curl.

The fix

const CreateUser = z.object({
  email: z.string().email(),
  name: z.string().min(1).max(100),
});

app.post("/api/users", requireAuth, async (req, res) => {
  const data = CreateUser.parse(req.body);      // allowlist, not passthrough
  const user = await db.user.create({ data: { ...data, role: "member" } });
  res.json(user);
});

An automated pass reads one file. An audit reads the system.

The tool above is deliberately narrow — one file, one dimension, one pass. The issues that actually take systems down live between files: in the trust boundary nobody documented, the query pattern that only emerges at scale, the cache that was correct until the second tenant arrived. That is what the Coreframe Technical Audit is for — five business days, a written report, a live findings call, and a remediation proposal you can execute with or without us.

£1,500–£2,500 solo · £3,000–£5,000 funded startup · £3,000–£6,000 SME

See what the full audit covers →

Related audits

All stack audits →