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
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 ¬AfterData[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
- 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.
- 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.
- 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.
- Victim calls wolfSSL_X509_notAfter(); XMEMCPY writes 32 bytes at notAfterData+2, spilling bytes 31-32 past the 5368-byte WOLFSSL_X509 heap chunk.
- 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
Triage and disclosure were performed by Calif.
- Verdict
- true positive
- Severity
- medium
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_GENhttps://github.com/wolfSSL/wolfssl/commit/452652bbc
Dates from discovery through public reveal.
- 2026-04-05 Sent to maintainer
- 2026-04-07 Patch released
- 2026-05-09 Maintainer acknowledged
- 2026-05-19 Reported to tracker
- 2026-05-20 Publicly revealed
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 ¬AfterData[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"
}