Weakness reference
CWE-566

Authorization Bypass Through User-Controlled SQL Primary Key

This weakness occurs when an application uses a user-supplied value such as a record ID in a URL or form parameter as a primary key to fetch database records…

01Summary

This weakness occurs when an application uses a user-supplied value (such as a record ID in a URL or form parameter) as a primary key to fetch database records, without verifying that the current user is authorized to access that specific record. An attacker can simply change the ID value to view or modify another user's data.

02How It Happens

Applications often retrieve user-specific data by accepting an ID parameter from the client — for example, a user ID, order ID, or profile ID passed in a URL query string or form field. If the application trusts this parameter directly and uses it to query the database without checking whether the logged-in user owns or has permission to access that record, an attacker can increment, decrement, or guess other ID values to access unauthorized data. The vulnerability exists because the authorization check (if any) happens *after* the record is fetched, or is missing entirely.

03Real-World Impact

An attacker can view sensitive personal information belonging to other users — account details, payment history, medical records, or private messages. In some cases, the same lack of authorization allows modification or deletion of other users' records. This violates confidentiality and integrity, and can lead to identity theft, financial fraud, or reputational harm.

04Vulnerable & Fixed Patterns

Vulnerable pattern
from flask import Flask, request
import sqlite3

app = Flask(__name__)

@app.route('/user_profile')
def get_profile():
    user_id = request.args.get('id')  # Attacker controls this
    conn = sqlite3.connect('app.db')
    cursor = conn.cursor()
    cursor.execute('SELECT name, email, phone FROM users WHERE id = ?', (user_id,))
    profile = cursor.fetchone()
    conn.close()
    return f"Name: {profile[0]}, Email: {profile[1]}"

Why it's vulnerable:
The code accepts a user-supplied id parameter and uses it directly to fetch a record, with no check that the logged-in user is authorized to view that specific profile. An attacker can change the id value to any integer and retrieve any user's data.

Fixed pattern
from flask import Flask, request, session
import sqlite3

app = Flask(__name__)

@app.route('/user_profile')
def get_profile():
    user_id = request.args.get('id')
    current_user_id = session.get('user_id')  # Get authenticated user
    
    conn = sqlite3.connect('app.db')
    cursor = conn.cursor()
    cursor.execute('SELECT name, email, phone FROM users WHERE id = ? AND id = ?', 
                   (user_id, current_user_id))
    profile = cursor.fetchone()
    conn.close()
    
    if not profile:
        return "Unauthorized", 403
    return f"Name: {profile[0]}, Email: {profile[1]}"
Vulnerable pattern
<?php
session_start();
$user_id = $_GET['id'];  // Attacker controls this

$mysqli = new mysqli('localhost', 'user', 'pass', 'app_db');
$result = $mysqli->query("SELECT name, email, phone FROM users WHERE id = $user_id");
$profile = $result->fetch_assoc();

echo "Name: " . $profile['name'] . ", Email: " . $profile['email'];
?>

Why it's vulnerable:
The code accepts a user-supplied id parameter and queries the database without verifying that the logged-in user owns or has permission to access that record. An attacker can change the id to any value and retrieve any user's profile.

Fixed pattern
<?php
session_start();
$user_id = $_GET['id'];
$current_user_id = $_SESSION['user_id'];  // Get authenticated user

$mysqli = new mysqli('localhost', 'user', 'pass', 'app_db');
$stmt = $mysqli->prepare("SELECT name, email, phone FROM users WHERE id = ? AND id = ?");
$stmt->bind_param('ii', $user_id, $current_user_id);
$stmt->execute();
$result = $stmt->get_result();
$profile = $result->fetch_assoc();

if (!$profile) {
    http_response_code(403);
    echo "Unauthorized";
    exit;
}

echo "Name: " . htmlspecialchars($profile['name']) . ", Email: " . htmlspecialchars($profile['email']);
?>

05Prevention Checklist

Always verify ownership:
Before returning or modifying a record, confirm that the authenticated user is the owner or has explicit permission to access it.
Use session/authentication context:
Retrieve the current user's ID from the session or authentication token, never from user input.
Include authorization in the query:
Add a WHERE clause that filters by both the requested ID *and* the current user's ID (or role/permission).
Return 403 Forbidden, not 404:
If a user requests a record they don't own, return a 403 status code rather than 404, to avoid leaking whether the record exists.
Test with multiple user accounts:
Manually verify that a logged-in user cannot access another user's records by changing ID parameters.
Use an access control framework:
Consider using a library or middleware that enforces authorization checks consistently across your application.

06Signs You May Already Be Affected

Check your application logs for repeated requests with incrementing or sequential ID values from a single IP address or user account — this pattern often indicates someone testing for this vulnerability. Review your code for any database queries that use user-supplied parameters (from URLs, forms, or API requests) without a corresponding authorization check in the same function or middleware layer.