You've just approved a PR. The logic was sound, the variable names were clear, and the tests passed. Three weeks later, someone on your team finds that any logged-in user can read any other user's order history. The code looked fine. That's the problem.
Security bugs don't announce themselves. They hide inside clean, idiomatic code that does exactly what it's supposed to do, as long as you only think about the happy path. Junior reviewers aren't missing these issues because they're careless. They miss them because spotting security flaws requires a different kind of attention than spotting logic errors, and that attention takes practice to build.
This post covers four vulnerability patterns that consistently slip through junior code reviews, why each one is easy to miss, and the specific habit that catches each one.
Why Security Bugs Survive Code Review
Most developer onboarding focuses on logic, style, and correctness. You learn to ask: does this do what the ticket says? Is it readable? Are there edge cases? Those are the right questions for most of what lands in a diff.
Security flaws require a different frame: who else could call this, and what could they make it do? That question doesn't come naturally when you're reading code that works. The logic executes cleanly in your head. The test passes. Nothing looks wrong because, functionally, nothing is wrong for the expected caller.
This isn't purely a knowledge gap. Most developers have read about OWASP's Top Ten. The problem is that reading about a vulnerability class and catching one under time pressure in a 300-line diff are genuinely different skills. The second requires pattern recognition, and pattern recognition comes from repetition. Most junior developers have simply reviewed too few PRs containing real vulnerabilities to have built that reflex yet.
Insecure Direct Object References
An insecure direct object reference (IDOR) happens when an endpoint accepts a user-supplied ID and uses it to fetch a record without checking whether the current user is allowed to access that record.
A typical example looks like this:
// Express route
app.get('/orders/:id', authenticate, async (req, res) => {
const order = await Order.findById(req.params.id);
res.json(order);
});
The authenticate middleware confirms the user is logged in. But there's no check that the order belongs to them. Any authenticated user can read any order by cycling through IDs.
This slips past review because the code is correct. It's clean. There's auth middleware. A reviewer who's mentally executing the happy path, a user requesting their own order, won't see anything wrong. The bug only appears when you ask: what happens if a different user calls this?
The habit that catches it: for every endpoint that fetches a record by ID, ask whether there's an ownership check after the fetch. findById(req.params.id) without a follow-up order.userId === req.user.id is a flag every time.
Mass Assignment Vulnerabilities
Mass assignment happens when a controller passes a request body or params object directly into a model update without specifying which fields are allowed to change.
Here's a Django example:
# View
def update_profile(request):
user = request.user
for key, value in request.data.items():
setattr(user, key, value)
user.save()
return Response({'status': 'updated'})
A user who knows the model structure can add "is_staff": true or "role": "admin" to that request body and elevate their own privileges. The server will write it without complaint.
Reviewers miss this because the code is short, and it looks like countless tutorial examples. The idiom feels normal. Rails' params.permit and Django REST Framework's explicit serializer fields exist precisely to prevent this pattern, but the unsafe version doesn't look obviously broken to someone who hasn't seen it exploited.
The habit: any time you see a model being created or updated from request data, check whether fields are explicitly permitted. If you don't see a whitelist, assume one is missing until you confirm otherwise.
Secrets and Credentials Left in Code
The obvious case, a plaintext API key committed directly in source, gets caught by most reviewers and by many CI tools. The subtle cases don't.
Consider a config file that's imported but not listed in .gitignore. Or a test helper that sets a default secret:
SECRET_KEY = os.environ.get('SECRET_KEY', 'dev-secret-do-not-use-in-prod')
That looks responsible. It reads from an environment variable. But if SECRET_KEY is ever unset in a deployed environment, the app falls back to a known default that's now in your public repository. Anyone who finds it can forge session tokens.
Env-var usage doesn't automatically mean safe. Reviewers should check two things: that the variable has no hardcoded fallback, and that the variable name appears in whatever secrets management or deployment configuration the project uses, not just in the code.
The habit: mentally grep for any string assignment near words like key, secret, token, or password, then trace where the value comes from. If it has a default, ask what happens when the environment doesn't set it.
Missing or Misconfigured Authentication Checks
This one comes in two forms. The first is a route with no auth middleware at all, which happens more often than you'd expect when someone adds a new endpoint in a hurry. The second is subtler: middleware is declared, but applied in the wrong order so it never runs.
An Express example of the ordering problem:
app.get('/admin/users', listUsers);
router.use(requireAdmin);
The requireAdmin middleware is there. A reviewer reads it and moves on. But because it's applied after the route is registered, it never executes for that route. The endpoint is open.
This survives review because reviewers read the middleware declaration and assume it works. Tracing execution order requires actively reading the sequence, not just confirming the middleware exists. That's a different cognitive task.
Frameworks handle this differently, which makes the pattern harder to internalize across stacks. Express depends on registration order. Django relies on decorator placement on individual views. Laravel groups routes under middleware in ways that can silently exclude a route if nesting is off. The failure mode looks different in each one.
The habit: for any new or modified route, trace the middleware stack from top to bottom before approving. Don't assume the existing pattern carries over to the new route.
How to Build the Reflex, Not Just the Knowledge
Knowing that IDOR exists is not the same as catching one. Reading this post will make you more alert to these patterns today. That alertness fades. What doesn't fade is the pattern recognition you build by actually working through real diffs and finding out what you missed.
The four habits above are learnable from documentation. But applying them under the time pressure of a real review, across different frameworks, in code written by someone else, requires repetition on real code. That's not a knock on documentation. It's just how motor skills work, and code review is partly a motor skill.
Stack-specific practice matters more than people expect. An IDOR looks different in a Django class-based view than in an Express route than in a Laravel resource controller. The abstraction layers are different. The idiomatic patterns are different. Drilling mass assignment in Rails and then reviewing a Django app gives you incomplete preparation, because the surface where the bug appears isn't the same.
This is where Goodcatch fits. It offers graded reviews on real pull requests, organized into stack-specific tracks across React, Vue, Svelte, Angular, Laravel, Django, Rails, Node, Spring Boot, ASP.NET Core, Next.js, and more. Each track includes eight reviews across multiple difficulty tiers. You find out not just whether you passed, but what you missed and what that pattern means for your review habits.
The unlimited plan's weak-spot analytics tell you which vulnerability classes you're still missing across multiple reviews, so you're not just accumulating reps but getting directional feedback on where your attention still slips.
You can try your first graded review in-browser with no account required to see how the grading works before you commit to anything.
One honest caveat: if you're already leading security-focused reviews at your company, or you're preparing for a dedicated application security role, a purpose-built appsec certification program will go deeper than Goodcatch. Programs like SANS SEC522 or OWASP's own training are built specifically for that context. Goodcatch is for developers who want to sharpen their review practice across the full range of bugs that show up in real PRs, security included.
Try a graded review in your stack now, no account needed.