Introduction: When Small Bugs Become Big Problems

Welcome to the final lesson! In our previous sessions, we acted like scouts. In the second lesson we mapped the network infrastructure, in the third we enumerated hidden API endpoints, and in the fourth we intercepted live traffic. We have successfully identified where the application lives and how it communicates. Now, it is time to use that knowledge to demonstrate real risk.

In professional penetration testing, you will often find vulnerabilities that seem minor on their own. A developer might dismiss a small information leak or a glitch in a search bar as "low priority." However, hackers do not attack vulnerabilities in isolation. They combine them.

This concept is called Vulnerability Chaining. It involves taking several low-severity issues and connecting them like puzzle pieces to create a high-severity exploit. In this lesson, we will simulate a real-world scenario where we combine a simple IDOR (Insecure Direct Object Reference) with an SQL Injection in a search feature to escalate our privileges and steal the entire database.

Recognizing Low-Severity Vulnerabilities

Before we build our chain, we need to identify the weak links. During a security assessment, you act as a detective looking for clues. Two very common "clues" you might find are IDOR and overlooked Injection points in non-critical features.

IDOR stands for Insecure Direct Object Reference. This happens when an application exposes a reference to an internal database object, like a User ID (e.g., /users/105), but fails to check if the person requesting it actually has permission to see it. Individually, this might just leak a username or an email address. While not good, it is often not considered a critical emergency by itself.

SQL Injection (SQLi) is a high-severity vulnerability and a staple of the OWASP Top 10 — a list of the most critical security risks in web applications. We often look for this in login forms to bypass passwords. However, developers are getting better at securing login forms. They often forget to secure less critical features, like a Search Snippets bar. They assume that because the search bar only looks at public notes, an injection there will not matter. We are about to prove that assumption wrong.

Step 1: Discovering IDOR Through User Enumeration

The first step in our chain is information gathering. We suspect the application uses sequential User IDs (1, 2, 3...) to identify accounts. If the application does not verify who is asking, we can simply ask for every id to see what comes back through User Enumeration.

We will use a simple shell script to loop through numbers 1 to 10 and send a request for each one. We authenticate using our own $TOKEN, but we ask for other users' profiles.

# Step 1: Discover IDOR - enumerate user IDs
# We loop through numbers 1 to 10
for id in $(seq 1 10); do
  # We request the user profile for each ID
  curl -s "http://localhost:3000/api/users/$id" \
    -H "Authorization: Bearer $TOKEN"
done

If the application is vulnerable to IDOR, the server will reply with JSON data for each user, even though we are logged in as someone else.

{"id":1, "username":"admin", "role":"admin"}
{"id":2, "username":"alice", "role":"user"}
{"id":3, "username":"bob", "role":"user"}
...

This is our first win. We now know that id: 1 is the admin. We have turned a blind guess into a concrete target. This information is vital for the next steps.

Step 2: Finding SQL Injection in Search Functionality

Now that we know who our target is (the admin), we need a way to access their data. We turn our attention to the application's search feature. The application allows users to search through text snippets.

We test for SQL Injection by sending a special character that is significant in database syntax: the single quote '. If the developer has not sanitized the input, this quote will break the SQL query structure, often causing the server to crash or return an error.

# Step 2: Find SQL injection in search
# We use --data-urlencode to safely encode the special character
curl --get "http://localhost:3000/api/snippets" \
  --data-urlencode "search=test'--" \
  -H "Authorization: Bearer $TOKEN"

In the command above, test' tries to close the text string in the database query early. The -- is a comment character in SQL, which tells the database to ignore everything that comes after our injection. We use --data-urlencode to ensure the special characters are properly encoded for the URL.

If the server returns a database error or behaves unexpectedly (like showing all results instead of none), we have confirmed the vulnerability. We now have a way to talk directly to the database.

Building the Attack Chain: Combining IDOR and SQLi

We have two pieces of the puzzle: we know the Admin's ID is 1 (from the IDOR), and we have a way to alter database queries (from the SQLi). Now we chain them together.

Normally, when you search for snippets, the backend code likely adds a rule like WHERE userId = current_user. This prevents you from seeing other people's private notes. However, because of the injection, we can rewrite that rule.

We will inject a payload that says: "Search for nothing, OR give me results where the userId equals 1."

# Step 3: Chain - Extract other users' private snippets via SQLi
# The payload is: ' OR userId=1--
curl --get "http://localhost:3000/api/snippets" \
  --data-urlencode "search=' OR userId=1--" \
  -H "Authorization: Bearer $TOKEN"

Here is what happens inside the database. The original query might look like this: SELECT * FROM snippets WHERE body LIKE '%search%' AND userId = 4;

After our injection, the query becomes: SELECT * FROM snippets WHERE body LIKE '%' OR userId=1--' AND userId = 4;

Because we added OR, the database returns any record where userId is 1, ignoring the fact that we are actually user 4. We have successfully bypassed the privacy checks and stolen the admin's private notes.

Escalating the Chain: From Snippets to Credentials
Documenting Attack Paths

As a penetration tester, your job is not just to break things, but to explain how you broke them so they can be fixed. This is called Documentation. When reporting a vulnerability chain, you must clearly articulate the steps.

A good report does not just say "You have SQL Injection." It tells a story:

  1. Recon: "We identified that User ID enumeration allows attackers to find target account IDs."
  2. Entry: "We identified an SQL injection flaw in the search endpoint."
  3. Exploit: "By chaining these, we bypassed authorization checks."
  4. Impact: "This allowed full extraction of administrative credentials."

Clear Documentation helps business leaders understand that fixing the "small" bugs is actually a high priority.

Breaking the Chain: Understanding Sequelize ORM

Now we put on our defender hats. To stop this attack chain, we need to apply Defense in Depth. This means implementing multiple layers of security so that if one fails, another catches the attack.

Before we look at the fix, let's understand the tool we'll use. Sequelize is an ORM (Object-Relational Mapper) — a library that lets you interact with databases using JavaScript objects instead of writing raw SQL strings. When you use Sequelize's built-in methods, it automatically parameterizes your queries, meaning user input is always treated as data, never as executable code.

Sequelize provides an Op (Operators) object that contains safe equivalents of SQL operators. For example:

  • Op.like is the safe version of SQL's LIKE
  • Op.eq is the safe version of =
  • Op.or is the safe version of OR

When you write { title: { [Op.like]: '%searchterm%' } }, Sequelize converts this to a parameterized query where searchterm can never break out and become SQL code.

Breaking the Chain: Implementing Layered Defenses

We will modify the backend code to fix both the SQL Injection and the IDOR logic. The key changes are:

  1. Replace raw SQL with Sequelize's findAll() method — this automatically parameterizes queries
  2. Always enforce userId from the authenticated session — this prevents IDOR

Here is the secure implementation. Notice how we extract the userId from the JWT token (the same pattern used throughout our codebase) and pass it directly to the query:

import express from 'express';
import jwt from 'jsonwebtoken';
import { Op } from 'sequelize'; // Import the Operators object
import { Snippet } from '../models/Snippet';
import { JWT_SECRET_KEY } from '../../config';

const router = express.Router();

router.get('/', async (req, res) => {
  // Authentication: Extract userId from JWT token
  const authHeader = req.headers.authorization;
  if (!authHeader) {
    return res.status(401).json({ error: "Missing authorization header" });
  }
  
  const token = authHeader.split(' ')[1];
  let decoded: any;
  try {
    decoded = jwt.verify(token, JWT_SECRET_KEY);
  } catch (error) {
    return res.status(401).json({ error: "Invalid token" });
  }
  
  // Get userId from the verified token - this is trusted data
  const userId = decoded.userId;
  
  try {
    const { search } = req.query;
    
    // Defense 1: Sequelize ORM handles parameterization automatically
    // Defense 2: userId comes from the token, not user input - prevents IDOR
    const snippets = await Snippet.findAll({
      where: {
        userId, // Always scope to current user - cannot be bypassed
        ...(search && {
          // Op.like safely parameterizes the search term
          // Even if search contains ' OR 1=1--, it's treated as literal text
          title: { [Op.like]: `%${search}%` }
        })
      }
    });
    
    res.json(snippets);
  } catch (error) {
    console.error("Error retrieving snippets:", error);
    res.status(500).json({ error: "Failed to retrieve snippets" });
  }
});

In the code above, even if a user tries to send ' OR userId=1--, Sequelize will treat that entire string as the title to search for. It will not execute it as SQL commands. The userId is strictly set from decoded.userId, which comes from our JWT verification — this is trusted server-side data that the attacker cannot manipulate.

Summary and Practice Preview

In this lesson, we explored Vulnerability Chaining. We learned that:

  • Low-severity bugs like IDOR and minor SQL injections become critical when combined.
  • Enumeration allows us to find valid targets (like Admin IDs).
  • SQL Injection can be used to bypass logic checks (OR 1=1) or steal data from other tables (UNION).
  • Sequelize ORM provides safe database operations through parameterized queries using the Op operators.
  • Layered Defense using parameterized queries and strict user scoping effectively breaks the chain.

You have seen how a few lines of bad code can lead to a total system compromise. Now it is your turn to practice. In the upcoming exercises, you will write the script to perform this attack chain against a test server, and then you will fix the vulnerability to secure the application.

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal