Page MenuHome GnuPG

gpg-agent stack buffer overflow in pkdecrypt using KEM
Closed, ResolvedPublic

Description

A crafted CMS (S/MIME) EnvelopedData message carrying an oversized wrapped session key (AES Key Wrap ciphertext) can cause a stack buffer overflow in gpg-agent during PKDECRYPT --kem=CMS handling.

Security Advisory: gpg-agent stack buffer overflow via PKDECRYPT --kem=CMS
(ECC KEM)
Vulnerability Type: Stack-based Buffer Overflow
Affected Software: GnuPG
Severity: High
Date: 18 Jan 2026
Discoverer: OpenAI Security Research (OutboundDisclosures@openai.com)

Summary
A crafted CMS (S/MIME) EnvelopedData message carrying an oversized wrapped
session key (AES Key Wrap ciphertext) can cause a stack buffer overflow in
gpg-agent during PKDECRYPT --kem=CMS handling. The root cause is an
unbounded session key length derived from attacker-controlled ciphertext
and used as the AES Key Wrap output length when decrypting into a
fixed-size stack buffer. libgcrypt’s AES-KW unwrap copies
attacker-controlled bytes into the caller-provided output buffer before
performing integrity checks, so the stack overflow occurs even if later CMS
or key-wrap integrity validation fails.

Affected Versions

  • Software Name: GnuPG
  • Confirmed Version(s): 2.5.17-beta2, commit:

d97e52cc7fc5cc7bd8629643fdf37105891ae201, date: 2025-12-30T17:52:43+01:00

  • Likely Affected: gpg-agent, gpgsm
  • Introduced Commit (If relevant): N/A

Vulnerability Details:
Component: gpg-agent (agent) / S/MIME (gpgsm)

Affected code:

  • agent/pkdecrypt.c in the PKDECRYPT --kem=CMS path (agent_kem_decrypt →

ECC KEM unwrap)

  • Reachable via sm/call-agent.c sending PKDECRYPT --kem=CMS

Root cause:
agent_kem_decrypt() trusts an attacker-supplied CMS wrapped-key length to
size an AES Key Wrap unwrap into a fixed 256-byte stack buffer.

Execution flow:

  1. gpgsm parses the CMS encryptedKey and forwards it to gpg-agent via

Assuan (PKDECRYPT --kem=CMS).

  1. gpg-agent derives encrypted_sessionkey_len = (nbits + 7) / 8 from an

opaque MPI.

  1. It computes sessionkey_len = encrypted_sessionkey_len - 8 (AES-KW

removes one 64-bit block).

  1. It calls gcry_cipher_decrypt with sessionkey as the output buffer, where

sessionkey is a fixed-size stack array (unsigned char sessionkey[256]).

  1. libgcrypt’s AES-KW unwrap begins by copying inbuflen - 8 bytes from the

ciphertext into the output buffer using memmove, overflowing the stack when
sessionkey_len exceeds 256.

Relevant code snippet (agent/pkdecrypt.c):
gcry_cipher_hd_t hd;
unsigned char sessionkey[256];
size_t sessionkey_len;

encrypted_sessionkey = gcry_mpi_get_opaque (encrypted_sessionkey_mpi,
&nbits);
encrypted_sessionkey_len = (nbits+7)/8;

err = gcry_cipher_open (&hd, algo, GCRY_CIPHER_MODE_AESWRAP, 0);

if (is_pgp && encrypted_sessionkey[0] != encrypted_sessionkey_len - 1)

{
  err = gpg_error (GPG_ERR_INV_DATA);
  goto leave;
}

err = gcry_cipher_setkey (hd, kek, kek_len);
sessionkey_len = encrypted_sessionkey_len - 8 - !!is_pgp;
if (!err)

err = gcry_cipher_decrypt (hd,
                           sessionkey, sessionkey_len,
                           encrypted_sessionkey + !!is_pgp,
                           encrypted_sessionkey_len - !!is_pgp);

gcry_cipher_close (hd);

Relevant libgcrypt behavior (cipher/cipher-aeswrap.c):
If outbuflen != inbuflen - 8, return error. Otherwise:
memmove(outbuf, inbuf + 8, inbuflen - 8);

This initial memmove copies attacker-controlled bytes into the output
buffer before any integrity checks.

Trigger conditions

  • Recipient uses ECC KEM for CMS (for example

dhSinglePass-stdDH-sha1kdf-scheme, sha256, sha384, or sha512 variants with
AES-KW).

  • The CMS encryptedKey (wrapped CEK) is attacker-controlled, a multiple of

8 bytes, and large enough that encrypted_sessionkey_len - 8 > 256 (for
example encrypted_sessionkey_len >= 272; 400 bytes in the proof of concept).

  • gpgsm forwards the ciphertext to gpg-agent via PKDECRYPT --kem=CMS with

no length gate on the CMS path.

  • This issue is not reachable in the KEM_PGP path because a 1-byte length

prefix check effectively caps encrypted_sessionkey_len to 256 bytes.

Proof-of-Concept (PoC)
A Docker-based reproducer and AddressSanitizer crash bundle is available at:
https://drive.google.com/file/d/1eC-VOCmfeqCFq26tGm6SXD3YUgIRD9lK/view

PoC behavior:

  • Decrypting a CMS message with a 400-byte wrapped key reliably triggers a

stack buffer overflow in gpg-agent.

  • gpgsm forwards the crafted CMS message to gpg-agent, which crashes during

PKDECRYPT --kem=CMS handling.

Observed output excerpt:
gpgsm: encrypted to nistp256 key 9A0D49D91EBEA6C59E9CE32C053CC090B01500BE
gpgsm: error decrypting session key: End of file
gpgsm: decrypting session key failed: End of file
gpgsm: message decryption failed: End of file <GpgSM>
[GNUPG:] FAILURE gpgsm-exit 50331649

ASan or Debug Output
AddressSanitizer reports a stack-buffer-overflow originating from memmove
in libgcrypt during AES-KW unwrap, writing beyond the 256-byte sessionkey
stack buffer in ecc_kem_decrypt. The overflow occurs before CMS or AES-KW
integrity checks complete.

Impact

  • Reliable denial of service via gpg-agent crash when decrypting crafted

CMS messages.

  • gpg-agent is a long-running, high-value process performing private-key

operations, so memory corruption represents a plausible code-execution
primitive depending on platform and hardening.

  • Malicious senders who encrypt CMS messages themselves can fully control

the bytes written past the stack buffer because they derive the same KEK as
the victim.

  • Passive or man-in-the-middle attackers can also trigger the overflow and

partially influence overwritten data due to the initial ciphertext copy
performed before integrity checks.

Recommendation

  • Add strict bounds checks before computing or using sessionkey_len.
  • Reject CMS inputs where sessionkey_len exceeds the size of the sessionkey

buffer, or allocate the buffer dynamically after validating length.

  • Validate CEK sizes against CMS expectations (legitimate CEKs are 16, 24,

or 32 bytes for AES-128/192/256).

  • Require AES-KW ciphertext lengths consistent with those CEK sizes.
  • Additional hardening: check encrypted_sessionkey for NULL, ensure

early-error paths close cipher handles, and prefer secure memory for key
material.

Timeline

  • January 2026: Discovered
  • January 2026: Reproduced and confirmed
  • January 2026: Advisory drafted

This information is being shared by OpenAI solely for the purpose of
improving security and reducing potential harm. This information is
presented as-is. We make no representations or warranties, express or
implied, as to the completeness, accuracy, or fitness for any particular
purpose of the information. This includes, without limitation, any
suggestions or ideas presented on how to remedy or mitigate an identified
vulnerability, including whether such suggestions or ideas would be
effective and/or could have other negative impacts.
OpenAI disclaims any liability for direct or indirect damages arising from
the reliance on, or use, misuse, or interpretation of this information. Any
references to third-party systems, services, or entities are included
solely for identification purposes and do not imply endorsement,
responsibility, or attribution.

Event Timeline

On 2026-01-20, I found the message to security@gnupg.org of:
Message-ID: 4e708880-04ac-45bc-8d16-6b585f2652a1n@aisle.com
in may spam folder. It has a 10MB long attachment. That might be one of reasons to be identified as a spam.

It also address this bug.

werner renamed this task from Security (internal) - gpg-agent stack buffer overflow to gpg-agent stack buffer overflow in pkdecrypt using KEM.Tue, Jan 20, 12:10 PM
werner edited projects, added gnupg26; removed gnupg.

We have no CVE yet. However, CVE is also a good tag for security bugs,

Affected versions are 2.5.13 to 2.5.16. The other branches are not affected.

I have this fix committed to my working directory:

@@ -745,7 +745,7 @@ ecc_kem_decrypt (int is_pgp, ctrl_t ctrl, const char *desc_text,
   unsigned char *kek = NULL;
   size_t kek_len;
 
-  gcry_cipher_hd_t hd;
+  gcry_cipher_hd_t hd = NULL;
   unsigned char sessionkey[256];
   size_t sessionkey_len;
   gcry_buffer_t kdf_params = { 0, 0, 0, NULL };
@@ -841,10 +841,16 @@ ecc_kem_decrypt (int is_pgp, ctrl_t ctrl, const char *desc_text,
   err = gcry_cipher_setkey (hd, kek, kek_len);
   sessionkey_len = encrypted_sessionkey_len - 8 - !!is_pgp;
   if (!err)
-    err = gcry_cipher_decrypt (hd, sessionkey, sessionkey_len,
-                               encrypted_sessionkey + !!is_pgp,
-                               encrypted_sessionkey_len - !!is_pgp);
+    {
+      if (sessionkey_len > sizeof sessionkey)
+        err = gpg_error (GPG_ERR_TOO_LARGE);
+      else
+        err = gcry_cipher_decrypt (hd, sessionkey, sessionkey_len,
+                                   encrypted_sessionkey + !!is_pgp,
+                                   encrypted_sessionkey_len - !!is_pgp);
+    }
   gcry_cipher_close (hd);
+  hd = NULL;
 
   if (err)
     {
@@ -868,6 +874,7 @@ ecc_kem_decrypt (int is_pgp, ctrl_t ctrl, const char *desc_text,
   mpi_release (encrypted_sessionkey_mpi);
   gcry_free (kdf_params.data);
   gcry_sexp_release (s_skey);
+  gcry_cipher_close (hd);
   xfree (shadow_info);
   return err;
 }
werner changed the task status from Open to Testing.Wed, Jan 21, 10:20 AM
werner shifted this object from the Restricted Space space to the S1 Public space.Wed, Jan 21, 12:23 PM
werner updated the task description. (Show Details)
werner changed the edit policy from "Custom Policy" to "Custom Policy".
werner changed the visibility from "g10code (Project)" to "Public (No Login Required)".
werner changed the edit policy from "Custom Policy" to "Contributor (Project)".