Lorem, ipsum dolor sit amet consectetur adipisicing elit. Qui, itaque voluptate ipsa non enim amet ducimus voluptatibus deserunt nam esse!
Finding Security Bugs in Code You Don't Remember Writing

Finding Security Bugs in Code You Don't Remember Writing

pr0h0
securitycode-reviewdebuggingsoftware-engineering
AI Usage (86%)

When I open a codebase I did not write, I do not start with naming conventions or folder structure. I start by asking a simpler question: where does untrusted input turn into a privileged action?

That framing usually gets me to the real bugs faster than reading every file. Unfamiliar code hides security issues because the dangerous part is rarely the obvious UI. It is usually the spot where a request handler, job worker, or helper function quietly trusts data that came from a client, a queue, or another service.

Why unfamiliar code hides security bugs

New codebases often look messy for harmless reasons. Different teams write in different styles. Old endpoints survive refactors. A feature flag keeps dead code around. None of that is a security issue on its own.

The problem is that the same mess makes it easy to miss a trust boundary. A function named updateProfile may also accept role, orgId, or accountId. A background job may read from Redis and immediately call a payment API. A route may parse JSON and pass a nested object into an ORM without checking whether the current user owns the record.

If you only scan for style, you miss the security decisions buried in the data flow.

Start with trust boundaries, not style

I usually draw three boxes:

  • inputs
  • privileged actions
  • storage or side effects

Then I connect them.

Map inputs, outputs, and privileged actions

In a JavaScript app, the inputs are usually easy to spot:

  • req.body, req.query, req.params
  • WebSocket events
  • form submissions
  • message queue payloads
  • webhook bodies
  • environment-derived config
  • data pulled from a database and later treated as trusted

The privileged actions are the calls that matter:

  • writing to another user's record
  • sending email or SMS
  • changing roles, billing state, or access flags
  • executing a shell command
  • reading files
  • calling internal services

Once you map those, the review becomes less about “is this code clean?” and more about “what happens if this value is attacker-controlled?”

Fast ways to reconstruct the system

You do not need the whole architecture on day one. You need the shortest path from request to effect.

Trace request flow from handler to storage

Start at the route or entry point and follow the first hop outward:

app.post("/api/invite", async (req, res) => {
  const { teamId, email, role } = req.body;
  await inviteUser({ teamId, email, role, actorId: req.user.id });
  res.json({ ok: true });
});

The dangerous question is not what inviteUser does in general. It is whether role is validated against the actor's permission level, whether teamId is checked for ownership, and whether the helper reuses trusted assumptions from the UI.

I like to trace:

  1. handler
  2. service function
  3. repository or ORM call
  4. external side effect

The bug often shows up between step 2 and 3, where business logic gets flattened into a generic helper.

Identify auth checks and assumptions

Look for the first auth check, then ask what it actually covers.

A common failure looks like this:

  • the route checks that the user is logged in
  • the handler accepts accountId
  • the database update uses accountId directly
  • nobody checks that accountId === req.user.accountId

That is not a login bug. It is an authorization bug hidden behind a valid session.

Bug patterns I look for first

Missing authorization on object access

This is the classic broken object reference problem, and it still shows up constantly.

const invoice = await db.invoice.findUnique({
  where: { id: req.params.id }
});

res.json(invoice);

If the route only checks that the requester is authenticated, any user who guesses another invoice ID may read it. The fix is usually boring: scope the lookup by both object ID and owner, or verify ownership after the read and before returning data.

State changes triggered by client-controlled values

I get suspicious any time the client can choose a state transition.

Examples:

  • status: "paid"
  • isAdmin: true
  • plan: "enterprise"
  • approvedBy: userId
  • discountPercent: 100

Sometimes the UI hides those fields, but the API still accepts them. That is why I test the backend directly. The frontend is a hint, not a control.

Unsafe deserialization, shelling out, and dynamic execution

These are rarer, but the impact is higher.

In JavaScript, the risky patterns are easy to recognize:

  • JSON.parse() on untrusted data is usually fine, but custom revivers and polymorphic object merging are not
  • child_process.exec() with string concatenation is a shell injection risk
  • eval(), Function(), and dynamic template execution should trigger a hard stop
  • vm and plugin systems need a very narrow trust model

If I see these, I stop reading for readability and start reading for exploitability.

A practical review workflow in JavaScript

Grep targets and code paths worth reading first

I use a boring shortlist:

  • req.body
  • req.query
  • req.params
  • findUnique, findFirst, update, delete
  • exec, spawn, eval, Function
  • role, admin, owner, permission, scope
  • webhook, job, worker, queue

That gets me to the code paths most likely to contain security decisions.

A good trick is to search for writes first, then trace backward to see where the inputs came from. Reads matter too, but writes show impact faster.

Small tests that confirm impact safely

You do not need a destructive payload to prove a bug.

Use a second low-privilege account and test whether it can:

  1. read another user's object
  2. change a field it should not control
  3. trigger an action out of scope for its role
  4. submit a malformed value and observe whether the server trusts it

Keep the test minimal. If a route accepts teamId, try another team's ID. If a job consumes JSON, change one field at a time. The goal is to isolate the missing check, not create noise.

💪

If a bug is real, the smallest request that crosses the trust boundary is usually the best proof.

How to write a useful security finding

A useful report is specific about three things:

  • what input the attacker controls
  • what privileged action happens because of it
  • what impact follows

I avoid vague language like “the app is insecure.” I write something closer to:

  • authenticated users can read invoices for other tenants
  • a low-privilege account can set role=admin through the API
  • a webhook payload can trigger file access outside the intended workspace

Then I include a short reproduction path and the exact missing check. If the issue is authorization, say where authorization belongs. If it is command execution, say where shelling out should be removed or replaced.

Closing the loop with a fix

The fix should usually land on the backend, not in the UI.

That means:

  • re-check ownership on the server
  • enforce allowed state transitions centrally
  • remove string-based shell execution
  • narrow deserialization and object merging
  • define explicit allowlists for sensitive fields

I also like to add a regression test that matches the exploit path. If the bug came from a handler passing accountId through to storage, the test should cover that exact route and a second account. That keeps the patch from drifting during the next refactor.

Further Reading

Share this post

More posts

Comments