Resources

What a code audit finds that a linter doesn’t

What a code audit finds that a linter doesn’t

This Express route passes ESLint’s recommended rules without a single warning:

router.get("/invoices/:id", requireLogin, async (req, res) => {
  const invoice = await db.invoice.findUnique({
    where: { id: req.params.id },
  });
  if (!invoice) return res.status(404).end();
  res.json(invoice);
});

It also lets any logged-in user read anyone’s invoice. Change the ID in the URL and the server hands it over.

A linter won’t catch that, and nobody should expect it to. ESLint’s docs describe its job as finding runtime bugs, broken best practices and style issues. It has no idea who your users are or what each of them may see.

So keep the linter. Just don’t read a green pipeline as proof that the app is safe to launch or hand over. A code audit starts where the linter stops. Someone reads the code and runs it, looking for what could lose you users or money. These are the things that tend to turn up, each with a check you can try on your own repo.

Who can see whose data

Broken access control is number one on the OWASP Top 10:2025. OWASP says 100% of the applications it tested had some form of it. The route above is the textbook case, known as an insecure direct object reference.

The fix is small. Tie the query to the account:

const invoice = await db.invoice.findFirst({
  where: { id: req.params.id, accountId: req.user.accountId },
});

Finding every place that needs it takes longer, because no tool knows your product’s rules. Create two test accounts. Log in as A and copy the IDs from your API responses. Log in as B and replay those requests. Anything that returns data is a bug. Write a test for each one you fix, so the next refactor can’t bring it back.

Whether anyone paid

This route is lint-clean too:

router.get("/checkout/success", requireLogin, async (req, res) => {
  await db.order.update({
    where: { id: Number(req.query.order) },
    data: { status: "paid" },
  });
  res.render("thanks");
});

Any logged-in user can open that URL with an order number and mark the order as paid. It fails honest customers as well. Stripe’s fulfillment guide warns that you can’t rely on the landing page, because a customer can pay and lose their connection before it loads.

An order should turn to paid only when your server receives a signed event from the payment provider. To check, search the code for every write to that status. You want very few of them, and each one should sit behind a signature check.

Secrets in old commits

A linter reads the code as it is today. Git keeps everything. A live key committed in March and deleted in April is still in every clone.

git log --all -p -S 'sk_live_'   # one pattern you already know
gitleaks git -v                  # the whole history

If Gitleaks finds a real key, replace the key before you do anything else. GitHub’s guide to removing sensitive data makes that the first step and says it may be all you need. Then look at what ships to the browser. Anyone who opens dev tools can read a key in your front-end bundle.

Whether it runs without the person who wrote it

Try this on a clean machine or in a throwaway container:

git clone <repo-url> fresh && cd fresh
npm ci && npm run build && npm test

Then start the app using only the README. Write down each thing you had to ask someone: the env var that lives in a chat message, the seed script, the Node version. The next developer will pay for that list in lost days, and no lint rule counts it.

While you’re in there, look at what the tests cover. A coverage number tells you little, since it can come from testing helper functions. Ask a plainer question. If sign-up or payment broke tonight, would a test go red?

Copied logic and old libraries

A discount rule pasted into three files will drift. One day two of the copies disagree and a customer gets the wrong price. A copy-paste detector can point at the files, but deciding which copy is right takes a person who knows the feature.

npm auditlists known problems in your dependencies. An auditor sorts them. A flaw in a build tool and a flaw in your login library aren’t the same size of problem, even when the scanner labels them alike.

Things that aren’t in the repo

Whose name is the domain in? Who can log in to the hosting, the Stripe account, the app stores? If the only admin is a contractor who left last year, a clean codebase won’t save you. List every outside service, write down whose name each account is in, and make sure at least two people at the company can log in.

Where the data goes

Draw the system on one page: the parts, the outside services, where the data lives. If nobody on the team can do that from memory, you’ve learned something already.

Then follow one user’s email address through it, and note each table, log and outside service that ends up with a copy. Check your error tracker and analytics scripts as well, since both can pick up personal data that nobody decided to send.

Code written with AI tools

Run these same checks on anything an AI tool wrote. In Veracode’s 2025 report, 45% of AI-written code samples failed security tests and added OWASP Top 10 flaws. AI tools write code fast, but AI alone doesn’t get an app to production quality. A senior engineer has to read the work and decide what is safe to ship.

What you should get at the end

A useful audit ends in a ranked list, worst first, in words a non-developer can follow. For each part of the product it says keep, fix or rebuild, with a rough number of hours for each fix. A raw scanner export isn’t an audit.

A senior developer who didn’t write the code can run everything above. If nobody has a free week, you can buy it. Ask one question before you do: does the price depend on the repair work? If the auditor only makes money when you hire them for the fix, the verdict is hard to trust. That’s why Rocketech, my company, sells its code audit services as a separate job, at a fixed $3,500 for one product of up to about 100,000 lines, with the report in 5 working days and nothing taken off later work. Whoever you pick, ask for the same split.

Start with one afternoon

Most of this fits in a few hours:

  1. Replay requests as a second user on your most sensitive routes.
  2. Search for every write to your “paid” status.
  3. Run Gitleaks over the full history.
  4. Build from a fresh clone.
  5. List your outside accounts and whose name each one is in.
  6. Follow one email address through the system.

Turn what you find into tests or tickets the same day, while you still remember the details.

Bogdan Sandu

Stay sharp. Ship better code.

Every week: one curated article, one tool worth knowing, one tip you can use tomorrow. No noise, no padding.