~ 6 min read

How to Hunt for IDOR Vulnerabilities To Exploit Security Misconfiguration?

share this story on
IDOR vulnerabilities are often overlooked but can lead to data exfiltration and exposure of confidential data. Here's how to hunt for them with an example Node.js code.

Perhaps one of the trickiest security vulnerabilities to detect are those that are based on business logic and are harder for code flow analysis to verify. Insecure Direct Object Reference (IDOR) is one such vulnerability that is often overlooked by developers and static analysis security scanners alike.

In this article I’ll explain what IDOR is and provide some real-world IDOR vulnerability code examples in Node.js and JavaScript to help you understand how to hunt for IDOR vulnerabilities in your codebase (maybe through a code review, or maybe helpful to security analysts).

What is IDOR ?

The security vulnerability known as Insecure Direct Object Reference (IDOR) is a type of vulnerability that occurs when developers fail to properly validate and authorize user access to system entities (known as objects), resulting in the application wrongly providing direct access to those said objects. Attackers exploit this type of security vulnerability to gain access to unauthorized data or resources, which often leads to exfiltration of sensitive information, and exposure of confidential data.

An IDOR Vulnerability Example #1

The following application code uses the Fastify framework and specifically has a route that allows users to retrieve a PDF document that was generated for them.

const Fastify = require("fastify");
const PDFDocument = require("pdfkit");
const fs = require("node:fs");
const fastify = Fastify();
fastify.register(require("@fastify/static"), { root: __dirname + "/static" });
fastify.register(require("@fastify/formbody"));
fastify.get("*", async () => {
return fastify.toRaw({ statusCode: 404, path: "/404.ejs" });
});
const port = process.env.PORT || 5000;
fastify.listen(port, "0.0.0.0", (err) => {
if (err) throw err;
console.log(`Listening on port ${port}...!!!`);
});

The above code provides the necessary boilerplate to setup a Fastify server and initialize the PDF generation library PDFKit. The server is configured to serve static files from the static directory and listen on port 5000.

Next up, let’s see how PDF documents are accessed through an HTTP endpoint:

fastify.post("/download", async (request) => {
const { pdfId } = request.body;
const id = parseInt(pdfId);
if (pdfIds.includes(id)) {
return fastify.download(`${id}.pdf`);
}
return { result: "Pdf not found. Try with another id between 1 and 1500." };
});

This route at /download accepts a POST request with a JSON payload containing a pdfId field. The frontend is likely taking in this pdfId from the user and sending it to the server. The web interface would look something like this:

idor vulnerability example code in Node.js

The server then checks if the pdfId is present in the pdfIds array which was pre-populated with PDF document IDs in the system, and if so, serves the corresponding PDF file. Alternatively, the server could’ve spawn up an SQL query against the database or a file system check to verify the existence of the PDF document.

So, what’s wrong with this?

An IDOR Vulnerability Example #2

Hopefully you already caught up with what is wrong with the above code and how this logic needs to be addressed. But let’s try another IDOR example which doesn’t revolve around object identifiers but rather around the user’s role and access level.

Consider the following controller code for deleting a message from the user’s inbox in a messaging application:

exports.delete_message = async (request, reply) => {
if (request.method !== "DELETE") {
return reply.code(400).send("Unaccepted request");
}
try {
// Get logged in user ID
const jwtToken = jwttoken(request.headers?.cookie);
// Get message user ID
const messageUser = await messageModel.findOne({ _id: request.params.id });
const messageUserId = messageUser?.user.toString();
// Check message existence
const message = await messageModel.findOne({ _id: request.params.id });
if (!message) {
return reply.code(400).send({ message: "Not found message" });
}
// Delete message
await messageModel.deleteOne({ _id: request.params.id });
return reply.code(200).send({ message: "Message deletion successful" });
} catch (err) {
return reply.code(500).send("Something went wrong");
}
};

What’s missing here?

Logically what is happening with the above controller code? It looks like this messaging removal function is used for a DELETE HTTP endpoint which is supposed to delete a message from the user’s own inbox by finding the message by its ID. Straight-forward. Until you realize the glaring security issue. What if the user is trying to delete a message that doesn’t belong to them? The code doesn’t check if the message belongs to the user before deleting it, which is a classic example of an IDOR vulnerability.

What we’re missing is an authorization check code block as follows before entering the message deletion logic:

// Compare message user ID with logged-in user ID
if (jwtToken._id !== messageUserId) {
return reply.code(403).send({ message: "Forbidden" });
}

IDOR and Broken Access Control

The issue with IDOR lies in the fact that the server is not properly validating the user’s access to the PDF document before serving it, hence it is creating an issue of broken access control - authorization which isn’t properly enforced. IDOR realistically manifests because the user is allowed to directly reference and provide any ID they want, such as 10 or 150201, until they hit a valid PDF document ID, which the server will happily serve.

IDOR are difficult to detect by automated security scanners because they don’t fall into the usual classification of source-to-sink injection like vulnerabilities. Instead, IDORs are based on a business logic. Who’s to say that this is an actual vulnerability? maybe everyone are indeed allowed to view every PDF document in the system, in which case this isn’t a problem. And so, applying the necessary context and system specification is required to determine if an IDOR vulnerability is actually present. Making secure code reviews and threat modeling an integral part of secure software development.

đź‘‹ Just a quick break

I'm Liran Tal and I'm the author of the newest series of expert Node.js Secure Coding books. Check it out and level up your JavaScript

Node.js Secure Coding: Defending Against Command Injection Vulnerabilities
Node.js Secure Coding: Prevention and Exploitation of Path Traversal Vulnerabilities

IDOR Mitigation

The following are some ideas of how to help mitigate IDOR vulnerabilities:

  1. RBAC - Role-based access control can help to enforce proper authorization and access control to system entities. Once you embed these as part of the underlying infrastructure of your application, it helps create awareness and a culture of security within your development team. I highly recommend taking a look at how permit.io offers a way to build permissions into an application by specifying the policy as code and allowing an easy way to achieve RBAC.
  2. DAST - Where-as SAST tools might eject many false positives in regards to IDOR, due to the very subtle context required to determine if an IDOR is present, Dynamic Application Security Testing (DAST) tools can help identify IDOR vulnerabilities by simulating real-world attacks against your application.
  3. Security Unit Tests - You can write security unit tests, such as those that specify user roles and access levels and then assert that the server is properly enforcing these roles and access levels. This can be a great way to ensure that your application is properly enforcing access control.

Node.js Security Newsletter

Subscribe to get everything in and around the Node.js security ecosystem, direct to your inbox.

    JavaScript & web security insights, latest security vulnerabilities, hands-on secure code insights, npm ecosystem incidents, Node.js runtime feature updates, Bun and Deno runtime updates, secure coding best practices, malware, malicious packages, and more.