Insecure Direct Object Reference(IDOR) vulnerability arises when the application allows users to access or modify objects that they are not authorized to do so. To exploit the IDOR vulnerability, it does not require an experienced hacker with custom malware or a terminal wizard, it usually just needs a curious user changing a single digit in a URL parameter in their web browser's address bar.
1. What is IDOR? A Bug Hiding in Plain Sight
Insecure Direct Object Reference(IDOR) is an access control vulnerability that arises when applications do not check for the user's permission to view or modify the object that is defined/referenced using a URL parameter.
Imagine you are logged into your bank's web application, and you are viewing your bank account balance from the URL https://mybank.com/dashboard/123345643. You are authenticated, and the application knows who you are. But, out of pure curiosity, you change the account number in the URL https://mybank.com/dashboard/123345644 and press the enter button.
And page loads and displays another person's dashboard with their account balance.
No injection, no malware, no scripts, no complicated payloads used. Just a small URL parameter change.
This is the simplicity of an Insecure Direct Object Reference (IDOR) vulnerability. This vulnerability is dangerous because the knowledge barrier required to exploit this vulnerability is extremely low, and it can often be used for horizontal privilege escalation.
2. The Core Root Cause (Confusing AuthN with AuthZ)
Authentication(AuthN) is not the same as Authorization(AuthZ). The most common IDOR vulnerabilities exist because of confusion between these two concepts.
Authentication:- The process of verifying the identity of the user. (Who are you?)
Authorization:- The process of determining if the user has permission to perform a specific action.
The application should never assume that the user has access automatically because the user has successfully logged in.
Frontend Validation is NOT a Security Boundary
One of the most common mistakes in application security is to delegate input validation to the frontend. Hiding something from view or validating a user input using JavaScript is good User Experience(UX), but not good Application Security. The browser is controlled by the user, user do not need to see the button to submit a malicious request. Just the endpoint and the variables that are accepted are enough.
if (user.isAdmin) {
showDeleteButton();
}
function showDeleteButton() {
const deleteButton = document.createElement("button");
deleteButton.textContent = "Delete Invoice";
deleteButton.onclick = () => {
// Sends a request to delete an invoice
fetch("/api/invoices/202", {
method: "DELETE",
headers: {
"Authorization": "Bearer [access_token]"
}
});
};
document.body.appendChild(deleteButton);
}User can manipulate user.isAdmin value using Developer Tools of the browser and successfully submit a DELETE request. Since the endpoint URL is exposed to the user, an attacker can bypass the JavaScript validation completely and submit the delete request to the server. If the server does not check the logged-in user's permissions, the attacker will be able to delete an invoice that does not belong to the current user by successfully exploiting the IDOR vulnerability.
Secure applications should not use the frontend validation for security and should hide both the UI elements and the code that gets executed.
// The Delete button-related code is displayed only to the admins
// Even the JavaScript code is not visible to ordinary users because the PHP is run on the server
// the server removes the DELETE button-related HTML and JavaScript code when the page is served to the client
// No endpoint exposure to unauthorized users
{% if user.isAdmin %}
showDeleteButton();
function showDeleteButton() {
const deleteButton = document.createElement("button");
deleteButton.textContent = "Delete Invoice";
deleteButton.onclick = () => {
// Sends a request to delete an invoice
fetch("/api/invoices/202", {
method: "DELETE",
headers: {
"Authorization": "Bearer [access_token]"
}
});
};
document.body.appendChild(deleteButton);
}
{% endif %}Never Trust Client-Supplied Ownership
A secure application should be careful when accepting ownership information from the client-side. When the user is authenticated, the server already knows about the user who has logged in. Then, the application should derive the user's identity from the session rather than trusting the information received by the client-side.
3. Code Breakdown - Vulnerable vs. Hardened
Most of the time, eliminating the IDOR vulnerability is a matter of one code line, one authorization check.
// Checks authentication, but ignores authorization
$invoiceId = $_GET['id'] ?? null;
// checking if the user is authenticated and logged in
if (!$authService->isAuthenticated()) {
http_response_code(401);
echo json_encode(['error' => 'Unauthorized']);
exit;
}
// Fetching the invoice using the URL parameter
// Vulnerable because the application does not check if the invoice belongs to the logged-in user
// User can manipulate the 'id' URL parameter and access any invoice in the database
$stmt = $pdo->prepare('SELECT * FROM invoices WHERE id = :id');
$stmt->execute(['id' => $invoiceId]);
$invoice = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$invoice) {
http_response_code(404);
echo json_encode(['error' => 'Invoice not found']);
exit;
}
echo json_encode($invoice);The above vulnerable code does not check the requested invoice against the logged-in user. An attacker can exploit this IDOR vulnerability to access all the invoices in the database regardless of the invoice ownership.
// Checks authentication
$invoiceId = $_GET['id'] ?? null;
$currentUserId = $authService->getUserId();
if (!$authService->isAuthenticated()) {
http_response_code(401);
echo json_encode(['error' => 'Unauthorized']);
exit;
}
// Invoice ownership is checked against the currently logged-in user
$stmt = $pdo->prepare('SELECT * FROM invoices WHERE id = :id AND user_id = :user_id');
$stmt->execute([
'id' => $invoiceId,
'user_id' => $currentUserId
]);
$invoice = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$invoice) {
// returns 404 error instead of 403 error
http_response_code(404);
echo json_encode(['error' => 'Invoice not found']);
exit;
}
echo json_encode($invoice);Returning 404 vs. 403
Another important application security design decision is what response the application should return when authorization fails. Many developers' automatic resolution is to return 403 Forbidden error response. While this response is technically correct, it can lead to object availability disclosure.
In an attacker's context:
GET /api/invoices/1001 returns 200 response --> Invoice Exists
GET /api/invoices/9001 returns 404 response --> Invoice Does Not Exists
GET /api/invoices/2001 returns 403 response --> Invoice Not Accessible but Exists
However, there is no universal rule that an endpoint should return a 404 error for every situation. Whether the endpoint should return a 404 response or a 403 response completely depends on the application's security requirements and design.
4. The UUID Myth - Why Obscurity Isn't Security
Replacing sequential numeric IDs with UUIDs is an improvement, but not a valid fix for IDOR vulnerability. Yes, guessing UUIDs using a script for a brute-force attack is statistically not possible. But a UUID is not a security token. It is an identifier.
There are many ways that a UUID can be leaked. Application logs, Screenshots, frontend JavaScript, email responses, third-party analytics tools, shared link or a clickable link in a document are some of the possibilities.
If the backend does not check for authorization, the resource can still be accessed.
5. The Hidden Danger - IDOR on State-Changing Actions
When discussing IDOR vulnerability, GET requests gets 99% of the attention. Reading someone else's invoice data, downloading another user's PDF files, accessing another user's private data, etc.
While read-only IDOR vulnerabilities are a major privacy violation, IDOR on state-changing operations (POST, PUT, PATCH, DELETE) is far more dangerous. It can transform a data-leak into a full-blown data-destruction or an account takeover.
Golden Rule to eliminate IDOR vulnerability is that the client-side should never decide who owns the resource. The ownership should always be derived from the authenticated session in the server.
6. How to Audit & Prevent IDOR
Knowing what an IDOR vulnerability is and how to eliminate it is easy, but actively eliminating it in a fast-growing code base with many developers working in parallel is a struggle. One common cause of IDOR is the lack of centralized authorization logic. One developer might remember to authorize the requests to the endpoint, while one might forget.
Centralize Authorization Logic
Instead of scattering the ownership authorization logic over multiple controllers, isolating the ownership logic in a single reusable class can help to eliminate the IDOR vulnerability. By abstracting the policy logic, any changes to how the ownership authorization works can be managed from one place rather than hundreds of individual controller functions.
class InvoicePolicy
{
public function view(User $user, Invoice $invoice): bool
{
// ownership logic check for invoices
return $user->id === $invoice->user_id;
}
}
// In controller or script
$invoice = $invoiceRepository->find($invoiceId);
if (!$policy->view($currentUser, $invoice)) {
http_response_code(404);
echo json_encode(['error' => 'Invoice not found']);
exit;
}Filter by Ownership at the Database Layer
Whenever possible, it is a good practice to retrieve the resources from the database based on the current user. This approach prevents the unauthorized objects from being retrieved from the database in the first place. It also reduces the risk of the developer forgetting to perform authorization checks before returning results to the frontend.
Instead of SELECT * FROM invoices WHERE id=?; use SELECT * FROM invoices WHERE id=? AND user_id=?; when possible.
7. Final Thoughts - Every Request is a Authorization Check
IDOR (Insecure Direct Object Reference) is a type of Broken Access Control vulnerability and falls under the Broken Access Control category in the OWASP Top 10, one of the most critical and commonly exploited web application security risks. It resides in the OWASP Top 10 not because it's difficult to understand, but because it's easy to overlook.
When the applications grow, new endpoints and functionalities get added, features gets relased under tight deadlines. All it takes is one forgotten ownership check for a user to get access to data or functionality that was never meant for them.
The best defence is not to rely on hidden URLs, UUIDs, JavaScript validation or hoping attackers will not notice, it is to always treat every request as a authorization decision.