


























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).
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.
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:

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?
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" });
}
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.
The following are some ideas of how to help mitigate IDOR vulnerabilities:
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。