Post

Secure Code Review

Secure Code Review

Secure Code Review

1. Introduction

Secure code review is the cornerstone of modern application security. It’s not just about spotting eval() calls or SQL queries—it’s about deeply understanding how the application behaves, how logic is constructed, and where assumptions break. In real-world, large-scale systems, the code is modular, distributed, and often event-driven. Security auditors must go beyond syntax to assess business logic flaws, authentication boundaries, and data flow risks.


2. What is Secure Code Review?

Secure code review is the process of analyzing application source code to find security vulnerabilities—either manually, using automated tools, or both. While static analysis tools can catch known patterns, manual inspection is essential to catch context-specific issues like access control violations, broken business logic, and dangerous assumptions.

“Tools can find bugs. Humans find flaws.”


3. Mapping the Codebase: Understand Before You Review

Before diving into the code, answer the following:

Key QuestionPurpose
How is the code structured?Identify modules and responsibilities
Where does user input enter?Entry points = attack surface
How is data processed and stored?Trace potential data exposure
What is the control flow style?Event-driven? REST? Queue-based?

Example Project Layout (Node.js)

1
2
3
4
5
6
7
8
src/
├── controllers/
├── services/
├── routes/
├── middlewares/
├── models/
├── config/
└── utils/

Tip: Scan the routes and entry points first to determine where sensitive logic resides.


4. Entry Points and Trust Boundaries

Your code review should prioritize “trust boundaries”—the points where external input enters the system.

Common Trust Boundaries:

  • HTTP request parameters (req.body, req.query)

  • WebSocket messages

  • API tokens and cookies

  • File uploads

  • Environment configurations

Example: API Endpoint Vulnerability

1
2
3
4
5
//  Vulnerable: Missing authorization check
app.get('/api/users/:id', async (req, res) => {
  const user = await db.getUserById(req.params.id);
  res.json(user);
});

Secure Version

1
2
3
4
5
6
7
app.get('/api/users/:id', authMiddleware, async (req, res) => {
  if (req.user.id !== req.params.id) {
    return res.status(403).json({ error: 'Unauthorized access' });
  }
  const user = await db.getUserById(req.params.id);
  res.json(user);
});

5. Understand the Programming Paradigm

Real-world systems are often:

  • Object-Oriented (OOP): Organize behavior into encapsulated units

  • Event-Driven: Especially in Node.js, React, or backend message queues

  • Asynchronous: Using promises, async/await, callbacks

Implication for Reviewers:

  • No central “start” function

  • Need to trace event handlers and callbacks

  • Vulnerabilities can be hidden in edge-case handlers or race conditions


6. Business Logic Flaws: The Real Killers

Automated tools don’t catch these.

Example: Broken Discount Logic

1
2
3
if (coupon.applied && cart.total > 1000) {
  discount = coupon.amount;
}

Red Flags:

  • Who can apply the coupon?

  • Can the user control both coupon and cart.total?

  • Is coupon reuse checked?

  • What if total is manipulated via request tampering?


7. Authentication and Session Logic

Example: Weak JWT Handling

1
const token = jwt.sign({ id: user.id }, 'secretkey');

Problems:

  • Hardcoded secret

  • No expiry (exp claim)

  • No issuer/audience validation

Improved Version:

1
2
3
4
5
const token = jwt.sign(
  { id: user.id, role: user.role },
  process.env.JWT_SECRET,
  { expiresIn: '1h', issuer: 'myApp' }
);

Always validate the JWT server-side even if the client sends it. Don’t assume role from the token without re-checking permissions.


8. Handling Input: The Gateway to Exploits

Dangerous Pattern:

1
2
3
const user = await db.query(
  `SELECT * FROM users WHERE email = '${req.body.email}'`
);

Safer Version with Prepared Statements:

1
2
3
4
const user = await db.query(
  `SELECT * FROM users WHERE email = $1`,
  [req.body.email]
);

Always sanitize and validate user input. Libraries like Joi (JS), Cerberus (Python), or Zod (TS) help enforce schema validation.


9. Race Conditions and Asynchronous Flaws

Vulnerable Transfer Logic (Node.js)

1
2
3
4
socket.on('transferFunds', async ({ from, to, amount }) => {
  await withdraw(from, amount);
  await deposit(to, amount);
});

Exploit Vector:

If called in quick succession, race conditions might allow over-withdrawal or double-deposit.

Mitigation:

  • Use atomic database operations or transactions

  • Lock resources temporarily

  • Rate-limit socket events


10. Reviewing Test Code for Security Gaps

Security-relevant tests:

  • Input validation edge cases

  • Authentication bypass attempts

  • Privilege escalation tests

  • Denial-of-service simulations

  • Invalid token handling

Use mutation testing tools (e.g., Stryker, Mutant) to assess whether your tests can detect broken or altered logic.


11. Advanced Red Flags to Look For

RiskCode Pattern
Arbitrary file readfs.readFile(req.query.path)
Unsafe evaleval(req.body.script)
Insecure redirectsres.redirect(req.query.url)
SSRFaxios.get(req.body.url)
No rate limitMissing IP-throttling middleware
Sensitive keys in repo.env, API_SECRET, config.js

12. Automation + Manual = Balance

ToolPurpose
SemgrepRule-based static analysis
CodeQLCode query language for deep queries
SonarQube / SonarLintQuality + security smell detection
FindSecBugs (Java)Security plugin for SpotBugs
Bandit (Python)Static analyzer for Python code
AST ViewersAnalyze language-specific trees for patterns

13. Tips for Reviewing Large Codebases

  • Don’t read everything. Focus on modules relevant to the audit goal (e.g., auth, payments).

  • Use folder conventions to identify sensitive code (e.g., auth/, admin/, handlers/).

  • Trace feature flow, not file order: Follow the flow from UI -> Controller -> Service -> DB.

  • Identify dangerous patterns like global state mutations, hardcoded secrets, or bypassable guards.

  • Search for bad assumptions, not just bad syntax.


14. Conclusion: Think Like an Architect, Review Like an Attacker

Secure code review is both strategic and surgical. It requires:

  • Architectural understanding

  • Practical attacker mindset

  • Familiarity with frameworks and language quirks

  • Deep knowledge of the OWASP Top 10, CWE, and custom logic flaws

“The goal isn’t to find one bug. The goal is to understand how bugs emerge from assumptions.”


This post is licensed under CC BY 4.0 by the author.