From bd17511070fb39a67bfa19682affb765e706a974 Mon Sep 17 00:00:00 2001
From: Viktor Dukhovni <viktor@openssl.org>
Date: Wed, 29 Apr 2026 18:23:24 +1000
Subject: [PATCH] Reject oversized inputs in ASN1_mbstring_ncopy()

In ASN1_mbstring_ncopy() the destination size for BMPSTRING and
UNIVERSALSTRING output was computed by a signed left shift on an
int:

    outlen = nchar << 1;        /* MBSTRING_BMP  */
    outlen = nchar << 2;        /* MBSTRING_UNIV */

For nchar large enough the result is not representable in int.  In
the worst case (nchar == 0x40000000) nchar << 2 wraps to zero,
OPENSSL_malloc(1) is called, and traverse_string() then writes
4*nchar bytes into the one-byte allocation: a heap buffer
overflow.  The MBSTRING_UTF8 path computes outlen by summing
per-character byte counts in out_utf8(), and that sum can overflow
the same int under similarly large inputs.

Neither path is reachable from code that processes X.509
certificates through the DIRSTRING_TYPE mask used by
ASN1_STRING_set_by_NID(): UNIVERSALSTRING is absent from that
mask, and the UTF-8 sum requires inputs on the order of half a
gigabyte.  Reaching them needs an application that calls
ASN1_mbstring_copy()/ASN1_mbstring_ncopy() directly, or registers
a custom NID via ASN1_STRING_TABLE_add(), with an oversized
attacker-controlled input.

Add range checks before each shift and in out_utf8(), raising
ASN1_R_STRING_TOO_LONG at the point of detection.  Move the
existing ASN1_R_INVALID_UTF8STRING raise into out_utf8() too so
the two failure modes report distinct codes; the MBSTRING_UTF8
caller is left with cleanup only and now frees dest on error,
matching the BMP/UNIV branches.

Fixes CVE-2026-7383

Reviewed-by: Nikola Pajkovsky <nikolap@openssl.org>
Reviewed-by: Norbert Pocs <norbertp@openssl.org>
Reviewed-by: Daniel Kubec <kubec@openssl.foundation>
MergeDate: Mon Jun  8 13:59:47 2026
(cherry picked from commit d32350ae8ef7426718f5aa9e383d4b51398ee255)
---
 crypto/asn1/a_mbstr.c | 33 +++++++++++++++++++++++++++++++--
 1 file changed, 31 insertions(+), 2 deletions(-)

diff --git a/crypto/asn1/a_mbstr.c b/crypto/asn1/a_mbstr.c
--- a/crypto/asn1/a_mbstr.c
+++ b/crypto/asn1/a_mbstr.c
@@ -169,18 +169,41 @@ int ASN1_mbstring_ncopy(ASN1_STRING **out, const unsigned char *in, int len,
         break;
 
     case MBSTRING_BMP:
+        if (nchar > INT_MAX / 2) {
+            ERR_raise(ERR_LIB_ASN1, ASN1_R_STRING_TOO_LONG);
+            if (free_out) {
+                ASN1_STRING_free(dest);
+                *out = NULL;
+            }
+            return -1;
+        }
         outlen = nchar << 1;
         cpyfunc = cpy_bmp;
         break;
 
     case MBSTRING_UNIV:
+        if (nchar > INT_MAX / 4) {
+            ERR_raise(ERR_LIB_ASN1, ASN1_R_STRING_TOO_LONG);
+            if (free_out) {
+                ASN1_STRING_free(dest);
+                *out = NULL;
+            }
+            return -1;
+        }
         outlen = nchar << 2;
         cpyfunc = cpy_univ;
         break;
 
     case MBSTRING_UTF8:
         outlen = 0;
-        traverse_string(in, len, inform, out_utf8, &outlen);
+        ret = traverse_string(in, len, inform, out_utf8, &outlen);
+        if (ret < 0) { /* error already raised in out_utf8() */
+            if (free_out) {
+                ASN1_STRING_free(dest);
+                *out = NULL;
+            }
+            return -1;
+        }
         cpyfunc = cpy_utf8;
         break;
     }
@@ -260,9 +283,15 @@ static int out_utf8(unsigned long value, void *arg)
     int *outlen, len;
 
     len = UTF8_putc(NULL, -1, value);
-    if (len <= 0)
+    if (len <= 0) {
+        ERR_raise(ERR_LIB_ASN1, ASN1_R_INVALID_UTF8STRING);
         return len;
+    }
     outlen = arg;
+    if (*outlen > INT_MAX - len) {
+        ERR_raise(ERR_LIB_ASN1, ASN1_R_STRING_TOO_LONG);
+        return -1;
+    }
     *outlen += len;
     return 1;
 }
--