ANT-2026-6615Y595 · wolfssl/wolfssl

heap-buffer-overflow low

CVE-2026-5448 GHSA-5jqq-xpcr-q3r7

Severity Claude medium · Security research firm medium · Maintainer low

REPORT

Anthropic's analysis, sealed at approval. Disclosure to the maintainer was performed by Calif.

ANT-2026-6615Y595: Final Assessment: 2-Byte Heap Overflow in wolfSSL_X509_notAfter / wolfSSL_X509_notBefore

The public getters wolfSSL_X509_notAfter() and wolfSSL_X509_notBefore() at src/x509.c:4418-4449 serialize a certificate date as [type][length][data] into a 32-byte notAfterData/notBeforeData array, but the upstream parser (internal.c:13831-13835) clamps the date length to MAX_DATE_SZ=32 rather than 30, so the memcpy at offset +2 writes up to 34 bytes into a 32-byte field. Because notAfterData is the last field of the heap-allocated WOLFSSL_X509 struct in common builds, the 2 excess bytes spill past the allocation boundary. An attacker supplying a certificate with a 32-byte GeneralizedTime fully controls the two overflowed bytes (date validation is bypassed under verify=0 in the d2i path), which land on the adjacent heap chunk's size/prev_size metadata under glibc ptmalloc. Triggering requires a build with OPENSSL_EXTRA/KEEP_PEER_CERT/SESSION_CERTS (ubiquitous in practice) and an explicit application call to wolfSSL_X509_notAfter() — a WOLFSSL_ABI export used by JSSE but not invoked automatically during the TLS handshake.

Target

Project: wolfSSL
Location: wolfSSL_X509_notAfter() at src/x509.c:4446

Technical Details

ASAN: "WRITE of size 32 at 0x5220000015f8 ... 0 bytes after 5368-byte region" — the XMEMCPY at x509.c:4446 copies notAfter.length (up to 32) bytes starting at &notAfterData[2], leaving only 30 bytes of in-bounds space. The root cause is an off-by-2 mismatch: the parser/copy at internal.c:13832 and CheckDate() at asn.c:20252 cap the date at MAX_DATE_SZ/CTC_DATE_SIZE (32), but neither accounts for the 2-byte type+length prefix that the getter prepends into the same 32-byte buffer. The +2 header was accounted for elsewhere (asn.c:34471 uses MAX_DATE_SIZE+2), indicating an oversight at this sink.

Crash trace:

==403==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x5220000015f8
WRITE of size 32 at 0x5220000015f8 thread T0
    #1 0x... in wolfSSL_X509_notAfter /opt/wolfssl/./src/x509.c:4446:5
0x5220000015f8 is located 0 bytes after 5368-byte region
allocated by thread T0 here:
    #1 0x... in wolfSSL_X509_new_ex /opt/wolfssl/./src/x509.c:6171:27

Reproduction

  1. Craft a DER X.509 certificate whose notAfter (and optionally notBefore) is a 32-byte GeneralizedTime, e.g. '20250101000000.0000000000000000Z', placing the two desired overflow bytes at positions 31-32.
  2. Deliver the certificate to the victim — as a malicious TLS server's peer cert, or as input to any code path calling wolfSSL_X509_d2i() on untrusted DER.
  3. The d2i path parses with verify=0, so CheckDate errors are suppressed and the raw 32-byte date (arbitrary bytes) is stored into x509->notAfter via CopyDecodedToX509.
  4. Victim calls wolfSSL_X509_notAfter(); XMEMCPY writes 32 bytes at notAfterData+2, spilling bytes 31-32 past the 5368-byte WOLFSSL_X509 heap chunk.
  5. With heap grooming, the 2 bytes overwrite the low 16 bits of the next glibc chunk's size field (or adjacent object data on jemalloc/musl), enabling chunk-overlap / size-corruption exploitation on a later free().

[No reproducer or sanitizer output attached — request from security-cvd@anthropic.com if needed.]

Suggested Fix

Clamp the copy length at the sink: use min(x509->notAfter.length, CTC_DATE_SIZE - 2) before the XMEMCPY at src/x509.c:4446 (and the mirror for notBefore at :4429). Alternatively, clamp to MAX_DATE_SZ - 2 at the source in internal.c:13824/13832.

Acknowledgement

This vulnerability was discovered by Claude, Anthropic's AI assistant, and triaged by the Anthropic security team in collaboration with Anthropic Research. Please direct questions to security-cvd@anthropic.com and reference ANT-2026-6615Y595.


Reference: ANT-2026-6615Y595
Anthropic CVD Policy: https://www.anthropic.com/coordinated-vulnerability-disclosure

SECURITY RESEARCH FIRM ANALYSIS

Triage and disclosure were performed by Calif.

Verdict
true positive
Severity
medium
UPSTREAM FIX

The change that resolved this finding.

diff --git a/src/internal.c b/src/internal.c
index 516f7ccc683..d59559112c1 100644
--- a/src/internal.c
+++ b/src/internal.c
@@ -13743,11 +13743,34 @@ static int CopyREQAttributes(WOLFSSL_X509* x509, DecodedCert* dCert)
 }
 #endif /* WOLFSSL_CERT_REQ */
 
+/* Copy an ASN-encoded date (type + length + data) into a WOLFSSL_ASN1_TIME.
+ * srcDate: ASN date buffer where [0]=type, [1]=length, [2..]=date bytes.
+ * srcDateLen: total length of srcDate (0 means no date present). */
+static void CopyDateToASN1_TIME(const byte* srcDate, int srcDateLen,
+                                WOLFSSL_ASN1_TIME* dst)
+{
+    if (srcDateLen >= 2) {
+        /* Clamp the date length to the maximum allowed size.
+         * This needs to match the size of WOLFSSL_ASN1_TIME minus the 
+         * the type and length fields. */
+        const int maxSz = CTC_DATE_SIZE - 2;
+        const int copySz = (int)min(srcDate[1], maxSz);
+        dst->type = srcDate[0];
+        dst->length = copySz;
+        XMEMCPY(dst->data, &srcDate[2], copySz);
+    }
+    else {
+        dst->length = 0;
+    }
+}
+
 /* Copy parts X509 needs from Decoded cert, 0 on success */
 int CopyDecodedToX509(WOLFSSL_X509* x509, DecodedCert* dCert)
 {
     int ret = 0;
+#ifdef WOLFSSL_SEP
     int minSz;
+#endif
 
     if (x509 == NULL || dCert == NULL ||
         dCert->subjectCNLen < 0)
@@ -13820,22 +13843,10 @@ int CopyDecodedToX509(WOLFSSL_X509* x509, DecodedCert* dCert)
         x509->hwSerialNumSz = 0;
 #endif /* WOLFSSL_SEP */
 
-    if (dCert->beforeDateLen > 0) {
-        minSz = (int)min(dCert->beforeDate[1], MAX_DATE_SZ);
-        x509->notBefore.type = dCert->beforeDate[0];
-        x509->notBefore.length = minSz;
-        XMEMCPY(x509->notBefore.data, &dCert->beforeDate[2], minSz);
-    }
-    else
-        x509->notBefore.length = 0;
-    if (dCert->afterDateLen > 0) {
-        minSz = (int)min(dCert->afterDate[1], MAX_DATE_SZ);
-        x509->notAfter.type = dCert->afterDate[0];
-        x509->notAfter.length = minSz;
-        XMEMCPY(x509->notAfter.data, &dCert->afterDate[2], minSz);
-    }
-    else
-        x509->notAfter.length = 0;
+    CopyDateToASN1_TIME(dCert->beforeDate, dCert->beforeDateLen,
+        &x509->notBefore);
+    CopyDateToASN1_TIME(dCert->afterDate, dCert->afterDateLen,
+        &x509->notAfter);
 
     if (dCert->publicKey != NULL && dCert->pubKeySize != 0) {
         x509->pubKey.buffer = (byte*)XMALLOC(
@@ -14217,29 +14228,10 @@ int CopyDecodedAcertToX509(WOLFSSL_X509_ACERT* x509, DecodedAcert* dAcert)
     }
 
     /* Copy before and after dates. */
-    {
-        int minSz = 0;
-
-        if (dAcert->beforeDateLen > 0) {
-            minSz = (int)min(dAcert->beforeDate[1], MAX_DATE_SZ);
-            x509->notBefore.type = dAcert->beforeDate[0];
-            x509->notBefore.length = minSz;
-            XMEMCPY(x509->notBefore.data, &dAcert->beforeDate[2], minSz);
-        }
-        else {
-            x509->notBefore.length = 0;
-        }
-
-        if (dAcert->afterDateLen > 0) {
-            minSz = (int)min(dAcert->afterDate[1], MAX_DATE_SZ);
-            x509->notAfter.type = dAcert->afterDate[0];
-            x509->notAfter.length = minSz;
-            XMEMCPY(x509->notAfter.data, &dAcert->afterDate[2], minSz);
-        }
-        else {
-            x509->notAfter.length = 0;
-        }
-    }
+    CopyDateToASN1_TIME(dAcert->beforeDate, dAcert->beforeDateLen,
+        &x509->notBefore);
+    CopyDateToASN1_TIME(dAcert->afterDate, dAcert->afterDateLen,
+        &x509->notAfter);
 
     /* Copy the signature. */
     if (dAcert->signature != NULL && dAcert->sigLength != 0 &&
diff --git a/src/ssl_api_crl_ocsp.c b/src/ssl_api_crl_ocsp.c
index 0bd3237c66d..677aad46812 100644
--- a/src/ssl_api_crl_ocsp.c
+++ b/src/ssl_api_crl_ocsp.c
@@ -424,7 +424,7 @@ int wolfSSL_get_ocsp_producedDate_tm(WOLFSSL *ssl, struct tm *produced_tm) {
 
     if (ExtractDate(ssl->ocspProducedDate,
             (unsigned char)ssl->ocspProducedDateFormat, produced_tm, &idx,
-            MAX_DATE_SZ))
+            MAX_DATE_SIZE))
         return 0;
     else
         return ASN_PARSE_E;
diff --git a/src/x509.c b/src/x509.c
index 8fc8edc57b6..48720eda71d 100644
--- a/src/x509.c
+++ b/src/x509.c
@@ -4419,8 +4419,14 @@ const byte* wolfSSL_X509_notBefore(WOLFSSL_X509* x509)
 {
     WOLFSSL_ENTER("wolfSSL_X509_notBefore");
 
-    if (x509 == NULL)
+    if (x509 == NULL) {
         return NULL;
+    }
+
+    if (x509->notBefore.length < 0 ||
+            x509->notBefore.length > (int)sizeof(x509->notBeforeData) - 2) {
+        return NULL;
+    }
 
     XMEMSET(x509->notBeforeData, 0, sizeof(x509->notBeforeData));
     x509->notBeforeData[0] = (byte)x509->notBefore.type;
@@ -4437,8 +4443,14 @@ const byte* wolfSSL_X509_notAfter(WOLFSSL_X509* x509)
 {
     WOLFSSL_ENTER("wolfSSL_X509_notAfter");
 
-    if (x509 == NULL)
+    if (x509 == NULL) {
+        return NULL;
+    }
+
+    if (x509->notAfter.length < 0 ||
+            x509->notAfter.length > (int)sizeof(x509->notAfterData) - 2) {
         return NULL;
+    }
 
     XMEMSET(x509->notAfterData, 0, sizeof(x509->notAfterData));
     x509->notAfterData[0] = (byte)x509->notAfter.type;
@@ -16060,6 +16072,10 @@ int wolfSSL_X509_set_notAfter(WOLFSSL_X509* x509, const WOLFSSL_ASN1_TIME* t)
         return WOLFSSL_FAILURE;
     }
 
+    if (t->length < 0 || t->length > CTC_DATE_SIZE - 2) {
+        return WOLFSSL_FAILURE;
+    }
+
     x509->notAfter.type = t->type;
     x509->notAfter.length = t->length;
 
@@ -16074,6 +16090,10 @@ int wolfSSL_X509_set_notBefore(WOLFSSL_X509* x509, const WOLFSSL_ASN1_TIME* t)
         return WOLFSSL_FAILURE;
     }
 
+    if (t->length < 0 || t->length > CTC_DATE_SIZE - 2) {
+        return WOLFSSL_FAILURE;
+    }
+
     x509->notBefore.type = t->type;
     x509->notBefore.length = t->length;
 
diff --git a/wolfssl/internal.h b/wolfssl/internal.h
index 216b75bde1c..53981450e88 100644
--- a/wolfssl/internal.h
+++ b/wolfssl/internal.h
@@ -2471,10 +2471,6 @@ struct WOLFSSL_OCSP {
 };
 #endif
 
-#ifndef MAX_DATE_SIZE
-#define MAX_DATE_SIZE 32
-#endif
-
 typedef struct CRL_Entry CRL_Entry;
 
 #if defined(WOLFSSL_SM2) && defined(WOLFSSL_SM3)
@@ -5312,10 +5308,6 @@ typedef struct Arrays {
     #endif
 #endif
 
-#ifndef MAX_DATE_SZ
-#define MAX_DATE_SZ 32
-#endif
-
 typedef enum {
     STACK_TYPE_X509               = 0,
     STACK_TYPE_GEN_NAME           = 1,
@@ -6327,7 +6319,7 @@ struct WOLFSSL {
 #endif /* HAVE_TLS_EXTENSIONS */
 #ifdef HAVE_OCSP
         void*       ocspIOCtx;
-        byte ocspProducedDate[MAX_DATE_SZ];
+        byte ocspProducedDate[MAX_DATE_SIZE];
         int ocspProducedDateFormat;
         buffer      ocspCsrResp[1 + MAX_CHAIN_DEPTH];
     #if defined(OPENSSL_ALL) || defined(WOLFSSL_NGINX) || defined(WOLFSSL_HAPROXY)
diff --git a/wolfssl/wolfcrypt/asn.h b/wolfssl/wolfcrypt/asn.h
index 08dab649ae0..6c04b4d2c01 100644
--- a/wolfssl/wolfcrypt/asn.h
+++ b/wolfssl/wolfcrypt/asn.h
@@ -1272,7 +1272,7 @@ enum Misc_ASN {
     DSA_PARAM_INTS      =   3,     /* DSA parameter ints */
     RSA_PUB_INTS        =   2,     /* RSA ints in public key */
     MIN_DATE_SIZE       =  12,
-    MAX_DATE_SIZE       =  32,
+    MAX_DATE_SIZE       =  CTC_DATE_SIZE,
     ASN_GEN_TIME_SZ     =  15,     /* 7 numbers * 2 + Zulu tag */
 
 #ifdef WOLFSSL_CERT_GEN

https://github.com/wolfSSL/wolfssl/commit/452652bbc

TIMELINE

Dates from discovery through public reveal.

  1. 2026-04-05 Sent to maintainer
  2. 2026-04-07 Patch released
  3. 2026-05-09 Maintainer acknowledged
  4. 2026-05-19 Reported to tracker
  5. 2026-05-20 Publicly revealed
PROVENANCE

SHA-3-512 hash:

bfaf579a04a708cc492bf7ea857d05814b0c5713f6c98f9d79026fe2e6a91382574d2f01df0491068ff7e56f372eaad683f4ab6cc4e3c3894368a6756e4347e6

Committed 2026-04-05 16:37 PT

Revealed 2026-05-20 00:40 PT

Verify (download preimage.json)

Show preimage JSON
{
  "ant_id": "ANT-2026-6615Y595",
  "bug_class": "Heap Buffer Overflow",
  "claude_severity": "medium",
  "commit_sha": null,
  "created_at": "2026-05-20T01:20:34+00:00",
  "description": "The public getters wolfSSL_X509_notAfter() and wolfSSL_X509_notBefore() at src/x509.c:4418-4449 serialize a certificate date as [type][length][data] into a 32-byte notAfterData/notBeforeData array, but the upstream parser (internal.c:13831-13835) clamps the date length to MAX_DATE_SZ=32 rather than 30, so the memcpy at offset +2 writes up to 34 bytes into a 32-byte field. Because notAfterData is the last field of the heap-allocated WOLFSSL_X509 struct in common builds, the 2 excess bytes spill past the allocation boundary. An attacker supplying a certificate with a 32-byte GeneralizedTime fully controls the two overflowed bytes (date validation is bypassed under verify=0 in the d2i path), which land on the adjacent heap chunk's size/prev_size metadata under glibc ptmalloc. Triggering requires a build with OPENSSL_EXTRA/KEEP_PEER_CERT/SESSION_CERTS (ubiquitous in practice) and an explicit application call to wolfSSL_X509_notAfter() — a WOLFSSL_ABI export used by JSSE but not invoked automatically during the TLS handshake.",
  "discovered_at": "2026-03-26T00:00:00+00:00",
  "location": "wolfSSL_X509_notAfter() at src/x509.c:4446",
  "poc_sha256": null,
  "preimage_version": 1,
  "project": "wolfSSL",
  "reproduction": [
    "1. Craft a DER X.509 certificate whose notAfter (and optionally notBefore) is a 32-byte GeneralizedTime, e.g. '20250101000000.0000000000000000Z', placing the two desired overflow bytes at positions 31-32.",
    "2. Deliver the certificate to the victim — as a malicious TLS server's peer cert, or as input to any code path calling wolfSSL_X509_d2i() on untrusted DER.",
    "3. The d2i path parses with verify=0, so CheckDate errors are suppressed and the raw 32-byte date (arbitrary bytes) is stored into x509->notAfter via CopyDecodedToX509.",
    "4. Victim calls wolfSSL_X509_notAfter(); XMEMCPY writes 32 bytes at notAfterData+2, spilling bytes 31-32 past the 5368-byte WOLFSSL_X509 heap chunk.",
    "5. With heap grooming, the 2 bytes overwrite the low 16 bits of the next glibc chunk's size field (or adjacent object data on jemalloc/musl), enabling chunk-overlap / size-corruption exploitation on a later free()."
  ],
  "technical_details": "ASAN: \"WRITE of size 32 at 0x5220000015f8 ... 0 bytes after 5368-byte region\" — the XMEMCPY at x509.c:4446 copies notAfter.length (up to 32) bytes starting at &notAfterData[2], leaving only 30 bytes of in-bounds space. The root cause is an off-by-2 mismatch: the parser/copy at internal.c:13832 and CheckDate() at asn.c:20252 cap the date at MAX_DATE_SZ/CTC_DATE_SIZE (32), but neither accounts for the 2-byte type+length prefix that the getter prepends into the same 32-byte buffer. The +2 header was accounted for elsewhere (asn.c:34471 uses MAX_DATE_SIZE+2), indicating an oversight at this sink.",
  "title": "Final Assessment: 2-Byte Heap Overflow in wolfSSL_X509_notAfter / wolfSSL_X509_notBefore",
  "vendor_severity": "medium"
}