Insecure Direct Object Reference (IDOR) is an access control vulnerability where an application exposes references to internal objects (like database keys) and fails to verify that users are authorized to access those objects. In this demo, the profile page allows any authenticated user to view any other user’s profile by simply changing the id parameter in the URL.
Continue until you get a “user not found” or error.
Use a simple script to enumerate all users:
import requestssession = requests.Session()# Login firstsession.post('https://auth-vulnerable.onrender.com/login', data={'username': 'usuario', 'password': 'password123'})# Enumerate usersfor user_id in range(1, 100): r = session.get(f'https://auth-vulnerable.onrender.com/profile?id={user_id}') if 'email' in r.text: print(f"[+] Found user ID {user_id}") # Extract data...
# Still vulnerable without authorizationfile_token = request.args.get('token') # UUIDfile = get_file_by_token(file_token)# If no ownership check, still vulnerable
Note: Non-sequential IDs don’t prevent IDOR, they just make it harder to exploit.
Mass Assignment IDOR
Pattern: Modifying unauthorized fields
# Vulnerable user update@app.route('/update_profile', methods=['POST'])def update_profile(): user = get_current_user() # Blindly accepting all form fields user.update(request.form) # VULNERABLE user.save()
The secure version implements proper authorization (secure/app.py:139-179):
@app.route('/profile')def profile(): # SECURE: Authentication check if 'user_id' not in session: flash('Debes iniciar sesión', 'warning') return redirect('/login') # Get requested profile ID requested_id = request.args.get('id', session['user_id']) # SECURE: Input validation try: requested_id = int(requested_id) except ValueError: flash('ID inválido', 'danger') return redirect('/dashboard') # SECURE: Authorization check - users can only view their own profile # unless they are admins if requested_id != session['user_id'] and session.get('role') != 'admin': flash('No tienes permiso para ver este perfil', 'danger') return redirect('/dashboard') connection = create_connection() if not connection: flash('Error de conexión', 'danger') return redirect('/dashboard') cursor = connection.cursor() # SECURE: Prepared statement + Limited data exposure query = "SELECT id, username, email, role, created_at FROM users WHERE id = %s" cursor.execute(query, (requested_id,)) user = cursor.fetchone() cursor.close() connection.close() if not user: flash('Usuario no encontrado', 'danger') return redirect('/dashboard') return render_template('profile.html', user=user)
Always verify that the current user is authorized to access the requested resource:
def check_authorization(current_user_id, requested_user_id, required_role=None): # Check if user is accessing their own resource if current_user_id == requested_user_id: return True # Check if user has required role (e.g., admin) if required_role and current_user.role == required_role: return True # Access denied return False# Usageif not check_authorization(session['user_id'], requested_id, 'admin'): abort(403) # Forbidden
2. Use Indirect References
Map direct object references to user-specific indirect references:
# Create session-specific mappingsession['document_refs'] = { 'a7f3b': 123, # Internal ID 'c9e2d': 456, 'f1a8b': 789}# User requests /document?ref=a7f3bdoc_ref = request.args.get('ref')if doc_ref not in session['document_refs']: abort(403)internal_id = session['document_refs'][doc_ref]document = get_document(internal_id)
3. Implement Access Control Lists (ACL)
Maintain explicit ownership/permission records:
# Database schemaCREATE TABLE resource_permissions ( user_id INT, resource_id INT, permission VARCHAR(20), -- 'read', 'write', 'delete' PRIMARY KEY (user_id, resource_id, permission));# Check permissiondef has_permission(user_id, resource_id, permission='read'): query = """ SELECT 1 FROM resource_permissions WHERE user_id = %s AND resource_id = %s AND permission = %s """ result = db.execute(query, (user_id, resource_id, permission)) return result is not None
4. Use UUIDs Instead of Sequential IDs
While not a complete fix, UUIDs make enumeration harder:
import uuid# In database schemaCREATE TABLE users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), username VARCHAR(50), ...);# URLs become/profile?id=f47ac10b-58cc-4372-a567-0e02b2c3d479# Still need authorization checks!
5. Filter Data by Owner
Always include ownership in queries:
# Bad: Fetches any document by IDquery = "SELECT * FROM documents WHERE id = %s"cursor.execute(query, (doc_id,))# Good: Only fetches documents owned by current userquery = "SELECT * FROM documents WHERE id = %s AND owner_id = %s"cursor.execute(query, (doc_id, current_user_id))
6. Implement Rate Limiting
Prevent automated enumeration attacks:
from flask_limiter import Limiterlimiter = Limiter( app, key_func=lambda: session.get('user_id'), default_limits=["100 per hour"])@app.route('/profile')@limiter.limit("10 per minute")def profile(): # ...