Security

The Top 10 Security Vulnerabilities a Code Audit Catches

The bugs that actually show up

Every security vendor has a top-ten list. Most of them are marketing wrapped around the OWASP Top 10, which is a fine document but describes categories, not the specific mistakes sitting in your repo right now.

This list is different in one way: it’s ordered by what we actually find when we read real production codebases at startups and scale-ups, not by theoretical severity. Some famous vulnerabilities (buffer overflows, say) barely appear in modern SaaS stacks. Others that get less press (broken object-level authorization) appear in nearly every codebase we open.

Here’s what a manual audit catches, and why half of it sails past automated scanners.

1. Broken object-level authorization (IDOR)

The single most common serious finding. The code checks that a user is logged in, but not that the resource they’re requesting is theirs:

// Vulnerable: any authenticated user can read any invoice
app.get('/api/invoices/:id', requireAuth, async (req, res) => {
  const invoice = await db.invoices.findById(req.params.id);
  res.json(invoice);
});

The fix is boring and must be everywhere:

const invoice = await db.invoices.findOne({
  _id: req.params.id,
  accountId: req.user.accountId,
});
if (!invoice) return res.sendStatus(404);

Scanners almost never catch this, because the code is syntactically fine: the bug is a missing line, and only a human tracing the data model knows it should be there. This is the heart of auditing authentication and authorization.

2. Secrets committed to the repository

API keys, database URLs, JWT signing secrets: in .env files that got committed, in config files, in test fixtures, or in a commit from 2023 that’s still in history even though the file was “deleted.” Git never forgets, and neither do the bots scraping public repos. We cover the mechanics (and the cleanup) in how exposed secrets sneak into your repo.

3. Injection: SQL and its cousins

Still alive in 2026, mostly at the edges: the one hand-built report query, the search endpoint with dynamic ORDER BY, the raw query someone wrote because the ORM was slow. String-concatenated SQL is the classic, but the same pattern shows up as NoSQL operator injection ({"$gt": ""} as a password), command injection in shell-outs, and template injection. There’s a full checklist in SQL injection and input validation.

4. Missing function-level authorization

The admin routes check for admins in the UI, by hiding the button. The API endpoint behind the button checks nothing. Any user who opens dev tools can call POST /api/admin/users/:id/impersonate directly. UI-enforced permissions are not permissions; they’re suggestions.

5. Vulnerable and abandoned dependencies

The average modern app is mostly other people’s code, and some of it is carrying known CVEs, some is unmaintained, and occasionally one package is doing something it shouldn’t. The audit question isn’t “are there CVEs?” (there always are) but “which ones are reachable in this codebase?” That triage is the entire subject of auditing your third-party packages.

6. Broken session and token handling

A greatest-hits collection: JWTs signed with a secret like "secret", tokens with no expiry, alg: none accepted by an old library version, session cookies missing HttpOnly/Secure/SameSite, and password-reset tokens that never get invalidated after use. Each one is a one-line fix; each one is invisible until someone looks.

7. Server-side request forgery (SSRF)

Any feature that fetches a user-supplied URL (webhooks, “import from URL”, link previews, PDF renderers) can be pointed at things the user shouldn’t reach: http://169.254.169.254/ (cloud metadata endpoints, which hand out credentials), internal admin services, or localhost. The fix is an allowlist of destinations plus blocking private IP ranges after DNS resolution, not naive hostname string checks.

8. Mass assignment

The handler that does User.update(req.body) for convenience, letting a request body quietly include "role": "admin" or "credits": 999999. Modern frameworks give you allowlisting (params.require(:user).permit(:name, :email) in Rails, explicit DTOs elsewhere); the vulnerability is not using it.

9. Sensitive data in logs and error responses

Stack traces returned to the client in production. Full request bodies, passwords included, shipped to a third-party logging SaaS. Database errors that echo the query back, giving an attacker a free schema tour. Individually these are “informational” findings; combined with anything else on this list, they’re the reconnaissance phase of a breach.

10. Missing rate limiting and abuse controls

No throttle on login (credential stuffing), on password reset (email bombing and token brute-force), on signup (spam), or on the expensive export endpoint (a self-inflicted denial of service). Rate limiting is unglamorous, which is exactly why it’s missing.

Why scanners miss half of this list

Look back at the list. Items 3, 5, and parts of 2 and 6 are pattern-shaped: a scanner can find string-concatenated SQL, known CVEs, and high-entropy strings. That’s why any competent audit runs those tools first.

But items 1, 4, 7, 8, and most of 9 and 10 are semantic. They’re not bad code; they’re missing code, or correct code in the wrong place. db.invoices.findById(id) is a perfectly valid line; it’s only a vulnerability because of what your data model means. No tool without a model of your business can flag it. That’s the structural reason manual review exists, and it’s why the review takes days rather than minutes, a tradeoff we’ve quantified in how long a code audit takes.

A quick self-assessment

Before any audit, you can grep your own repo for the cheap wins:

  • Search for findById, find_by_id, and raw WHERE id =: does each one also scope by owner/tenant?
  • Search for password, secret, api_key in tracked files and git history.
  • Check your JWT config: algorithm pinned? Expiry set? Secret from the environment?
  • Open your admin endpoints and ask: what enforces admin, the UI or the server?
  • Run npm audit / pip-audit / cargo audit and count the criticals you’ve been ignoring.

If any of those made you pause, that’s signal.

Get a free code audit

We run this exact review (all ten classes, by hand, against your actual codebase) as a genuinely free audit. No trial, no card, no “free assessment” that turns into a sales deck. A Webisoft engineer reads your repository and sends you a written report of findings, ranked by real-world risk, with concrete fixes. If you want an independent set of eyes on your code before someone less friendly provides one, get a free code audit.

FAQ

Frequently asked questions

What is the most common serious vulnerability found in real code audits?

Broken object-level authorization, often called IDOR: the code checks that a user is logged in but never checks that the resource they are requesting is theirs. It shows up in nearly every codebase because the vulnerable code looks perfectly normal; the bug is a missing ownership check, not a suspicious line.

Why do automated scanners miss vulnerabilities like IDOR and mass assignment?

Because these bugs are semantic, not pattern-shaped. A scanner can flag string-concatenated SQL or a known CVE, but a query that fetches an invoice by ID alone is syntactically valid code; it is only a vulnerability because of what your data model means. Catching it requires a human tracing who should be allowed to reach what, which is why manual review exists alongside the tools.

We deleted the file with the API keys. Are we safe?

Not yet. Git history keeps every version of every committed file, so a secret committed years ago is still retrievable even though the file is gone from the current tree. The keys need to be rotated, and if the history itself must be cleaned, that is a separate, more involved operation.

Is SQL injection still a real threat if we use an ORM?

Mostly, yes, at the edges. ORMs parameterize the common paths, but injection survives in the hand-built report query, the search endpoint with a dynamic ORDER BY, and the raw query someone wrote for performance. The same pattern also appears as NoSQL operator injection and command injection in shell-outs, so the audit looks wherever user input meets a raw string.

Can I check for some of these issues myself before getting an audit?

Yes, and it is worth doing. Grep for lookups by ID and confirm each one also scopes by owner or tenant, search tracked files and git history for secrets, verify your JWT config pins an algorithm and sets expiry, and run your package manager's audit command. That self-check catches the cheap wins; a free manual audit like ours covers the semantic classes a grep cannot see.

Free · no strings shown later

Get your free code audit

A repo URL and two minutes of your time. A Webisoft engineer sends back written findings (security, tech debt, performance) with file-and-line specifics.

Request my free audit