Dev.to Security 🔐 Cybersecurity 👁 0 📖 3 min read

The Security Bugs That Look Fine in Code Review

I recently finished 41 secure-coding labs across TypeScript, Java and C#. The biggest lesson was not about exotic exploits. The bugs that cause real damage usually look like perfectly reasonable code, and they pass revie

I recently finished 41 secure-coding labs across TypeScript, Java and C#. The biggest lesson was not about exotic exploits. The bugs that cause real damage usually look like perfectly reasonable code, and they pass review because every line makes sense on its own.
Here are five patterns that stood out, each with the one-line habit that prevents it. The examples use TypeScript and NestJS, but the ideas apply everywhere.

  1. Checking who you are, but not whose data it is Authentication answers "who is this?" Authorization answers "can they touch this object?" Many APIs do the first and forget the second. This is the classic broken object-level authorization (IDOR) bug. // Looks fine, but any logged-in user can read any invoice return this.invoices.findOne({ where: { id } });

// Scope the query to the owner
return this.invoices.findOne({ where: { id, ownerId: user.id } });
Habit: every query that loads something by ID should also be scoped to the caller, or go through an explicit permission check.

  1. Trusting a token because it "verifies" A token that passes signature verification is not automatically a token you should accept. Pin the algorithm, the issuer and the audience, and check expiry. jwt.verify(token, publicKey, { algorithms: ['RS256'], issuer: 'https://auth.example.com', audience: 'my-api', }); The same applies to identity headers set by a gateway. If your service trusts x-user-id, make sure nothing outside the gateway can set it. Habit: decide exactly what a valid token must contain, and reject everything else.
  2. Race conditions in business logic Imagine a voucher with a quota of one. Two requests arrive at the same moment, both read remaining = 1, and both succeed. Nothing was "hacked"; the logic simply was not atomic. // Read, check, then write: two requests can both pass the check const v = await repo.findOne({ where: { id } }); if (v.remaining > 0) { v.remaining--; await repo.save(v); }

// Let the database do the check and the write in one statement
const res = await db.query(
'UPDATE vouchers SET remaining = remaining - 1 WHERE id = $1 AND remaining > 0',
[id],
);
if (res.rowCount === 0) throw new ConflictException('Quota exhausted');
Habit: anything involving money, quotas or inventory needs an atomic update or a transaction with the right isolation.

  1. Webhooks that can be replayed Verifying a webhook signature proves it came from the sender. It does not prove it is new. An attacker who captures one valid request can send it again. Check the signature, check the timestamp is recent, and store event IDs so each one is processed once. Habit: signature plus timestamp plus idempotency key. All three.
  2. Letting clients decide which fields to set Mass assignment happens when you pass a request body straight into your model and a client adds "role": "admin". In NestJS you can reject unknown fields globally: app.useGlobalPipes( new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true }), ); Habit: define the fields a client may send, and drop or reject everything else. A small checklist Before I open a pull request now, I ask: • Is this query scoped to the caller? • Is every token or header I trust actually verified, and only for what it should prove? • Can two requests at once break this rule? • Could a captured request be replayed? • Can a client set a field it should not? Over to you Which of these have you seen in a real codebase? And what is the one security habit you wish you had learned earlier? I would love to hear it in the comments. I am a software engineer working with NestJS, TypeScript, Python and Soroban. You can find my work on GitHub at github.com/OthmanImam.
📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.