Related articles: Also see the OWASP context behind IDOR and the other top application vulnerabilities, a comprehensive guide to testing all forms of broken access control, and API security testing where IDOR vulnerabilities most commonly surface.
How to Test for Insecure Direct Object References (IDOR) Vulnerabilities
User Alice logs in. Her profile URL is /api/users/42. She changes it to/api/users/43. She now sees Bob's private data�email, address, order history. No authentication error. No authorization check. Just unrestricted access to anyone's data.
This is IDOR (Insecure Direct Object Reference), and it's everywhere.
IDOR consistently ranks in the OWASP Top 10 under "Broken Access Control." It's deadly simple: applications expose internal object IDs (database primary keys, file names) without checking if the current user should access them.
The scary part? 73% of APIs have IDOR vulnerabilities, according to security research. Most go undetected because:
- Functional tests only use valid user data
- Security scans don't test authorization logic
- Code review misses authorization checks in every endpoint
This guide shows you how to systematically test for IDOR vulnerabilities and implement authorization testing that catches them before attackers do.
Understanding IDOR
graph LR
A[User Request] --> B{Authentication?}
B -->|No| C[401 Unauthorized]
B -->|Yes| D{IDOR Check}
D -->|Vulnerable| E[Returns ANY Resource<br/>by ID]
D -->|Secure| F{Authorization?}
F -->|Unauthorized| G[403 Forbidden]
F -->|Authorized| H[Returns User's Resource]
style E fill:#ffccbc
style H fill:#c5e1a5
IDOR vs Proper Authorization
// ? IDOR VULNERABLE: No authorization check
app.get('/api/orders/:id', authenticate, async (req, res) => {
const order = await db.orders.findById(req.params.id);
res.json(order); // Returns ANY order by ID!
});
// ? SECURE: Checks resource ownership
app.get('/api/orders/:id', authenticate, async (req, res) => {
const order = await db.orders.findById(req.params.id);
if (!order) {
return res.status(404).json({ error: 'Order not found' });
}
// Authorization check
if (order.userId !== req.user.id && !req.user.isAdmin) {
return res.status(403).json({ error: 'Access denied' });
}
res.json(order);
});
Common IDOR Patterns
| Type | Example | Vulnerable Parameter |
|---|---|---|
| Numeric ID | /api/users/123 |
Sequential integer |
| UUID | /api/users/a1b2c3-... |
UUID (harder but still IDOR) |
| Filename | /files/invoice-2024.pdf |
Predictable filename |
/api/users/alice@example.com |
Email address | |
| Username | /api/profiles/@alice |
Username |
| Query Parameter | /api/data?userId=123 |
URL parameter |
| Body Parameter | {"accountId": 123} |
POST/PUT body |
The IDOR Testing Strategy
graph TD
A[Identify Resources] --> B[Map Endpoints]
B --> C[Test Horizontal Access]
C --> D[Test Vertical Access]
D --> E[Test State Changes]
E --> F[Automate Tests]
F --> G[Continuous Monitoring]
style A fill:#bbdefb
style C fill:#fff9c4
style D fill:#fff9c4
style E fill:#fff9c4
style F fill:#c5e1a5
1. Discovery: Finding IDOR Candidates
// idor-discovery.ts
import { Page } from '@playwright/test';
interface IDORCandidate {
endpoint: string;
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
idParameter: string;
idType: 'numeric' | 'uuid' | 'string';
idLocation: 'path' | 'query' | 'body';
exampleId: string;
}
class IDORDiscovery {
private candidates: IDORCandidate[] = [];
async discoverFromPage(page: Page): Promise<IDORCandidate[]> {
// Intercept all API requests
page.on('request', (request) => {
const url = new URL(request.url());
// Check path parameters
const pathIds = this.extractPathIds(url.pathname);
pathIds.forEach(({ param, type, value }) => {
this.candidates.push({
endpoint: this.normalizeEndpoint(url.pathname, param),
method: request.method() as any,
idParameter: param,
idType: type,
idLocation: 'path',
exampleId: value,
});
});
// Check query parameters
const queryIds = this.extractQueryIds(url.searchParams);
queryIds.forEach(({ param, type, value }) => {
this.candidates.push({
endpoint: url.pathname,
method: request.method() as any,
idParameter: param,
idType: type,
idLocation: 'query',
exampleId: value,
});
});
// Check body parameters (POST/PUT)
if (['POST', 'PUT', 'PATCH'].includes(request.method())) {
try {
const body = JSON.parse(request.postData() || '{}');
const bodyIds = this.extractBodyIds(body);
bodyIds.forEach(({ param, type, value }) => {
this.candidates.push({
endpoint: url.pathname,
method: request.method() as any,
idParameter: param,
idType: type,
idLocation: 'body',
exampleId: value,
});
});
} catch (e) {
// Not JSON body
}
}
});
return this.candidates;
}
private extractPathIds(pathname: string): Array<{ param: string; type: string; value: string }> {
const ids: Array<{ param: string; type: string; value: string }> = [];
const segments = pathname.split('/');
segments.forEach((segment, index) => {
// Numeric ID
if (/^\d+$/.test(segment)) {
ids.push({
param: segments[index - 1] || 'id',
type: 'numeric',
value: segment,
});
}
// UUID
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(segment)) {
ids.push({
param: segments[index - 1] || 'id',
type: 'uuid',
value: segment,
});
}
});
return ids;
}
private extractQueryIds(params: URLSearchParams): Array<{ param: string; type: string; value: string }> {
const ids: Array<{ param: string; type: string; value: string }> = [];
for (const [key, value] of params.entries()) {
if (key.toLowerCase().includes('id') || key.toLowerCase().includes('user')) {
ids.push({
param: key,
type: /^\d+$/.test(value) ? 'numeric' : 'string',
value,
});
}
}
return ids;
}
private extractBodyIds(body: any, prefix = ''): Array<{ param: string; type: string; value: string }> {
const ids: Array<{ param: string; type: string; value: string }> = [];
for (const [key, value] of Object.entries(body)) {
const fullKey = prefix ? `${prefix}.${key}` : key;
if (key.toLowerCase().includes('id') && typeof value === 'string') {
ids.push({
param: fullKey,
type: /^\d+$/.test(value) ? 'numeric' : 'string',
value,
});
}
if (typeof value === 'object' && value !== null) {
ids.push(...this.extractBodyIds(value, fullKey));
}
}
return ids;
}
private normalizeEndpoint(pathname: string, paramToReplace: string): string {
return pathname
.replace(/\/\d+/g, '/:id')
.replace(/\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, '/:id');
}
}
2. Horizontal IDOR Testing (Same Privilege Level)
// idor-horizontal-test.ts
import { test, expect } from '@playwright/test';
/**
* Horizontal IDOR: User A accessing User B's resources (same role)
*/
test.describe('Horizontal IDOR Tests', () => {
let userAToken: string;
let userBToken: string;
let userAOrderId: string;
let userBOrderId: string;
test.beforeAll(async ({ request }) => {
// Create two regular users
const userA = await request.post('/api/auth/register', {
data: {
email: 'alice@example.com',
password: 'SecurePass123!',
},
});
userAToken = (await userA.json()).token;
const userB = await request.post('/api/auth/register', {
data: {
email: 'bob@example.com',
password: 'SecurePass123!',
},
});
userBToken = (await userB.json()).token;
// Create orders for both users
const orderA = await request.post('/api/orders', {
headers: { Authorization: `Bearer ${userAToken}` },
data: { items: [{ productId: 1, quantity: 2 }] },
});
userAOrderId = (await orderA.json()).id;
const orderB = await request.post('/api/orders', {
headers: { Authorization: `Bearer ${userBToken}` },
data: { items: [{ productId: 2, quantity: 1 }] },
});
userBOrderId = (await orderB.json()).id;
});
test('should NOT allow user A to view user B order (GET)', async ({ request }) => {
const response = await request.get(`/api/orders/${userBOrderId}`, {
headers: { Authorization: `Bearer ${userAToken}` },
});
expect(response.status()).toBe(403); // Forbidden
const body = await response.json();
expect(body.error).toContain('Access denied');
});
test('should NOT allow user A to update user B order (PUT)', async ({ request }) => {
const response = await request.put(`/api/orders/${userBOrderId}`, {
headers: { Authorization: `Bearer ${userAToken}` },
data: { status: 'cancelled' },
});
expect(response.status()).toBe(403);
});
test('should NOT allow user A to delete user B order (DELETE)', async ({ request }) => {
const response = await request.delete(`/api/orders/${userBOrderId}`, {
headers: { Authorization: `Bearer ${userAToken}` },
});
expect(response.status()).toBe(403);
});
test('should NOT allow accessing other user via query parameter', async ({ request }) => {
// Many apps forget to check query params!
const response = await request.get('/api/orders', {
headers: { Authorization: `Bearer ${userAToken}` },
params: { userId: 'user-b-id' }, // Trying to access B's orders
});
const orders = await response.json();
// Should only see own orders
orders.forEach((order: any) => {
expect(order.userId).not.toBe('user-b-id');
});
});
test('should NOT allow modifying other user ID in request body', async ({ request }) => {
const response = await request.post('/api/orders', {
headers: { Authorization: `Bearer ${userAToken}` },
data: {
userId: 'user-b-id', // Attempting to create order for B
items: [{ productId: 3, quantity: 1 }],
},
});
const order = await response.json();
// Order should belong to A, not B
expect(order.userId).not.toBe('user-b-id');
});
});
3. Vertical IDOR Testing (Privilege Escalation)
// idor-vertical-test.ts
/**
* Vertical IDOR: Regular user accessing admin resources (privilege escalation)
*/
test.describe('Vertical IDOR Tests', () => {
let userToken: string;
let adminToken: string;
let adminId: string;
test.beforeAll(async ({ request }) => {
// Create regular user
const user = await request.post('/api/auth/register', {
data: { email: 'user@example.com', password: 'Pass123!' },
});
userToken = (await user.json()).token;
// Create admin user
const admin = await request.post('/api/auth/register', {
data: { email: 'admin@example.com', password: 'Pass123!', role: 'admin' },
});
const adminData = await admin.json();
adminToken = adminData.token;
adminId = adminData.userId;
});
test('should NOT allow regular user to access admin panel', async ({ request }) => {
const response = await request.get('/api/admin/dashboard', {
headers: { Authorization: `Bearer ${userToken}` },
});
expect(response.status()).toBe(403);
});
test('should NOT allow regular user to list all users', async ({ request }) => {
const response = await request.get('/api/admin/users', {
headers: { Authorization: `Bearer ${userToken}` },
});
expect(response.status()).toBe(403);
});
test('should NOT allow regular user to modify user roles', async ({ request }) => {
const response = await request.put(`/api/users/${adminId}`, {
headers: { Authorization: `Bearer ${userToken}` },
data: { role: 'admin' }, // Attempting privilege escalation
});
expect(response.status()).toBe(403);
});
test('should NOT allow regular user to delete other users', async ({ request }) => {
const response = await request.delete(`/api/users/${adminId}`, {
headers: { Authorization: `Bearer ${userToken}` },
});
expect(response.status()).toBe(403);
});
});
4. Automated IDOR Scanner
// idor-scanner.ts
interface IDORTestResult {
endpoint: string;
vulnerable: boolean;
severity: 'critical' | 'high' | 'medium';
details: string;
proof: any;
}
class IDORScanner {
async scanEndpoint(
candidate: IDORCandidate,
user1Token: string,
user2Token: string,
user1Id: string,
user2Id: string,
): Promise<IDORTestResult> {
// Test if user1 can access user2's resource
const url = this.buildTestUrl(candidate, user2Id);
const response = await fetch(url, {
method: candidate.method,
headers: {
Authorization: `Bearer ${user1Token}`,
'Content-Type': 'application/json',
},
});
const vulnerable = response.status === 200;
return {
endpoint: candidate.endpoint,
vulnerable,
severity: this.determineSeverity(candidate, vulnerable),
details: vulnerable ? `User 1 can access User 2's resource at ${url}` : `Properly secured: ${response.status}`,
proof: vulnerable ? await response.json() : null,
};
}
private buildTestUrl(candidate: IDORCandidate, targetUserId: string): string {
let url = `${process.env.API_BASE_URL}${candidate.endpoint}`;
if (candidate.idLocation === 'path') {
url = url.replace(':id', targetUserId);
} else if (candidate.idLocation === 'query') {
url += `?${candidate.idParameter}=${targetUserId}`;
}
return url;
}
private determineSeverity(candidate: IDORCandidate, vulnerable: boolean): 'critical' | 'high' | 'medium' {
if (!vulnerable) return 'medium';
// Critical if reading/modifying sensitive data
if (candidate.endpoint.includes('user') || candidate.endpoint.includes('account')) {
return 'critical';
}
// Critical if write operations
if (['DELETE', 'PUT', 'PATCH'].includes(candidate.method)) {
return 'critical';
}
return 'high';
}
async scanApplication(): Promise<IDORTestResult[]> {
console.log('?? Starting IDOR vulnerability scan...');
// Create two test users
const [user1, user2] = await this.createTestUsers();
// Discover IDOR candidates
const discovery = new IDORDiscovery();
const candidates = await discovery.discoverFromPage(/* page */);
console.log(`Found ${candidates.length} potential IDOR endpoints`);
// Test each candidate
const results: IDORTestResult[] = [];
for (const candidate of candidates) {
const result = await this.scanEndpoint(candidate, user1.token, user2.token, user1.id, user2.id);
results.push(result);
if (result.vulnerable) {
console.log(`?? VULNERABLE: ${candidate.endpoint}`);
}
}
const vulnerableCount = results.filter((r) => r.vulnerable).length;
console.log(`\n?? Scan complete: ${vulnerableCount}/${results.length} vulnerable endpoints`);
return results;
}
private async createTestUsers(): Promise<[{ token: string; id: string }, { token: string; id: string }]> {
// Implementation depends on your auth system
return [
{ token: 'user1-token', id: 'user1-id' },
{ token: 'user2-token', id: 'user2-id' },
];
}
}
5. CI/CD Integration
# .github/workflows/idor-scan.yml
name: IDOR Security Scan
on:
pull_request:
schedule:
- cron: '0 3 * * *'
jobs:
idor-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Start test environment
run: |
docker-compose up -d
npm run db:migrate
- name: Run IDOR scanner
run: |
npm install
npx ts-node security/idor-scanner.ts
- name: Check for vulnerabilities
run: |
VULNS=$(jq '.vulnerableCount' idor-report.json)
if [ "$VULNS" -gt 0 ]; then
echo "? Found $VULNS IDOR vulnerabilities"
exit 1
fi
Prevention Strategies
| Strategy | Implementation | Effectiveness |
|---|---|---|
| Authorization Middleware | Centralized access control | ????? |
| Indirect Object References | Map UUID to internal ID | ????? |
| Attribute-Based Access Control | Fine-grained permissions | ????? |
| Rate Limiting ID Enumeration | Slow down brute force | ????? |
| Audit Logging | Detect unauthorized access | ????? |
Secure Implementation Example
// Reusable authorization middleware
function authorizeResource(resourceType: string) {
return async (req: Request, res: Response, next: NextFunction) => {
const resource = await db[resourceType].findById(req.params.id);
if (!resource) {
return res.status(404).json({ error: 'Not found' });
}
// Check ownership
if (resource.userId !== req.user.id && !req.user.isAdmin) {
// Log unauthorized access attempt
await auditLog.log({
action: 'UNAUTHORIZED_ACCESS_ATTEMPT',
userId: req.user.id,
resource: resourceType,
resourceId: req.params.id,
});
return res.status(403).json({ error: 'Access denied' });
}
req.resource = resource;
next();
};
}
// Usage
app.get('/api/orders/:id', authenticate, authorizeResource('orders'), (req, res) => {
res.json(req.resource); // Already authorized
});
Conclusion
IDOR is one of the easiest vulnerabilities to introduce and one of the most damaging. 73% of APIs have them, but systematic testing catches them before attackers do.
Key takeaways:
- Test horizontal and vertical access for every resource
- Automate IDOR testing in CI/CD pipelines
- Implement authorization middleware for all endpoints
- Use indirect object references (UUIDs over sequential IDs)
- Log and monitor authorization failures
Start testing today:
- Run IDOR discovery on your app
- Test horizontal access (user A ? user B resources)
- Test vertical access (user ? admin resources)
- Automate tests in CI/CD
- Implement centralized authorization
Don't wait for a security researcher to report stolen data. Test for IDOR now.
Ready to automate security testing? Sign up for ScanlyApp and catch IDOR vulnerabilities before they become breaches.
