Note on disclosure channel. Filing publicly: there is no memory error here. The full reproduction below runs clean under -fsanitize=address,undefined -fno-sanitize-recover=all. The defect produces a wrong value, not a crash or an out-of-bounds access. See "On severity" at the end.
Summary
The overflow guard in oid_parse_number() tests the accumulator before the multiply, so it only rejects a numeric string whose running prefix exceeds UINT_MAX / 10 without having wrapped. Once a wrap occurs the accumulator is small again, the guard stops firing, and parsing continues.
The consequence is that for any target arc value N, the string 4294967296 followed by the decimal digits of N is accepted and encodes to N. mbedtls_oid_from_numeric_string() returns 0 (success) and the caller gets an OID with a different attribute type from the one requested.
Reachable directly from the bundled sample programs programs/x509/cert_write and programs/x509/cert_req via a command-line argument. The substitution is not visible in the resulting certificate — the certificate is well-formed and simply states the substituted OID.
System information
- Mbed TLS version:
development @ ce0384b99 (mbedtls-4.2.0-22-gce0384b99). git diff mbedtls-4.2.0 HEAD -- library/x509_create.c is empty, so released 4.2.0 is affected.
- Also affects 3.6 LTS. The function lives at
library/oid.c:990 in 3.6 and the loop body is character-for-character identical to 4.x. A backport will be needed.
- Operating system: Linux x86_64
- Configuration: default, unmodified
- Compiler: gcc,
cmake -DENABLE_PROGRAMS=On, default options
Expected behavior
include/mbedtls/oid.h:296-297 states:
\return #MBEDTLS_ERR_ASN1_INVALID_DATA if \p oid_str does not represent a valid OID
2.5.4294967296 has an arc beyond the range of unsigned int and is not an OID this encoder can represent, so it should be rejected with MBEDTLS_ERR_ASN1_INVALID_DATA.
Actual behavior
The function returns 0 and encodes 2.5.0. cert_write prints ok throughout and exits 0.
Steps to reproduce
cmake -S . -B build -DENABLE_PROGRAMS=On
cmake --build build --target cert_write cert_req -j8
openssl ecparam -name prime256v1 -genkey -noout -out ck.pem
openssl ecparam -name prime256v1 -genkey -noout -out sk.pem
./build/programs/x509/cert_write \
issuer_key=ck.pem subject_key=sk.pem \
issuer_name="CN=Issuer" \
subject_name="2.5.4294967296=#0C0141" \
not_before=20250101000000 not_after=20350101000000 \
output_file=defect.crt
openssl x509 -in defect.crt -noout -subject -nameopt oid
Output:
Running the same command with subject_name="2.5.0=#0C0141" produces a byte-identical certificate.
Additional information
Any attribute type can be produced
Prefixing an arc with 4294967296 wraps the accumulator to zero; the digits that follow then build the value normally. So 4294967296 + decimal digits of N yields arc N, at any position, in any OID. All of the following were produced with cert_write on an unmodified default build, exit code 0:
subject_name passed on the command line |
what the certificate contains |
2.5.4.42949672963=#0C0161 |
CN = a (commonName) |
2.5.4.42949672965=#0C0161 |
serialNumber = a |
2.5.4.42949672966=#0C0161 |
C = a (countryName) |
2.5.4.429496729610=#0C0161 |
O = a (organizationName) |
2.5.4.429496729611=#0C0161 |
OU = a (organizationalUnitName) |
2.5.4.4294967296132=#0C0161 |
2.5.4.132 = a |
1.2.840.113549.1.9.42949672961=#0C0161 |
emailAddress = a |
cert_req behaves identically, so CSRs are affected as well:
cert_req subject_name="2.5.4.4294967299=#0C0161" -> subject=CN = a
cert_req subject_name="2.5.4.3=#0C0161" -> subject=CN = a (byte-identical)
So a DN string that contains no recognisable attribute name, and no textual match for the target OID, can produce a certificate or CSR carrying any attribute type. The string-to-DER mapping is onto: every OID has an unbounded family of distinct decimal strings that encode to it.
Root cause
library/x509_create.c:288-296 (4.x), library/oid.c:996-1004 (3.6):
while (*p < bound && **p >= '0' && **p <= '9') {
ret = 0;
if (*num > (UINT_MAX / 10)) { /* tests the accumulator before the multiply */
return MBEDTLS_ERR_ASN1_INVALID_DATA;
}
*num *= 10;
*num += **p - '0'; /* the value actually used is *num * 10 + d */
(*p)++;
}
The guard constrains *num; the code uses *num * 10 + d. At *num == UINT_MAX / 10 == 429496729 the guard passes, *num * 10 == 4294967290, and adding d in {6,7,8,9} crosses UINT_MAX.
The important part is what happens next: after the wrap *num is {0,1,2,3}, which is far below the guard threshold, so the loop keeps consuming digits and building a new value from the wrapped one. A string is rejected only if some prefix exceeds 429496729 without having wrapped first — which is why 4294967300 is correctly rejected but 42949672966 is not.
Why the existing safeguards do not catch it
- The same function gets the equivalent check right 100 lines later, at
library/x509_create.c:406: if (component2 > (UINT_MAX - (component1 * 40))) — subtract first, then compare, which is the form the in-loop guard is missing.
- The existing boundary tests exercise a different check.
tests/suites/test_suite_x509write.data:336,339 pin 2.4294967215 as accepted and 2.4294967216 as rejected, but 2.4294967216 is rejected by the component2 check above, not by the in-loop guard. The in-loop guard has never been covered.
- There is no second line of defence downstream:
mbedtls_x509_string_to_names() treats a 0 return as success without re-validating the arcs.
Suggested regression tests
For tests/suites/test_suite_x509write.data, next to the existing pair:
oid_from_numeric_string:"2.4294967296":MBEDTLS_ERR_ASN1_INVALID_DATA:""
oid_from_numeric_string:"2.4294967299":MBEDTLS_ERR_ASN1_INVALID_DATA:""
oid_from_numeric_string:"2.42949672966":MBEDTLS_ERR_ASN1_INVALID_DATA:""
The first two come from a bounded model check of oid_parse_number with ESBMC: the property was "if the function reports success, the value it produces must equal the value the digit string denotes", with the input constrained only to ASCII digits, and the counterexample was "4294967299" — accepted, but yielding 3. The third covers the continue-after-wrap case, which a single-wrap check would miss.
Suggested fix
library/x509_create.c:290 (and the corresponding line in 3.6's library/oid.c):
- if (*num > (UINT_MAX / 10)) {
+ if (*num > (UINT_MAX - (unsigned int) (**p - '0')) / 10) {
return MBEDTLS_ERR_ASN1_INVALID_DATA;
}
On severity
I do not believe this is a vulnerability under the project's threat model, and I am not requesting security handling. SECURITY.md scopes security issues to memory corruption and undefined behaviour when parsing certificates, CSRs and CRLs, and states that Mbed TLS "must not be used to sign untrusted CSRs" and is "unsuitable for use in a Certificate Authority" — which is the only setting in which an adversary would be supplying the DN string.
Reporting it as a correctness defect: the documented \retval contract is not honoured, and the string-to-DER mapping is onto where the API's semantics require it to be injective. The practical consequence for anyone doing attribute-type filtering on the string form is that such a filter can be bypassed entirely, which seemed worth stating even though the setting is one the project already declines to support.
Duplicate search
Searched issues and pull requests. The closest hits do not cover this defect:
Summary
The overflow guard in
oid_parse_number()tests the accumulator before the multiply, so it only rejects a numeric string whose running prefix exceedsUINT_MAX / 10without having wrapped. Once a wrap occurs the accumulator is small again, the guard stops firing, and parsing continues.The consequence is that for any target arc value
N, the string4294967296followed by the decimal digits ofNis accepted and encodes toN.mbedtls_oid_from_numeric_string()returns 0 (success) and the caller gets an OID with a different attribute type from the one requested.Reachable directly from the bundled sample programs
programs/x509/cert_writeandprograms/x509/cert_reqvia a command-line argument. The substitution is not visible in the resulting certificate — the certificate is well-formed and simply states the substituted OID.System information
development@ce0384b99(mbedtls-4.2.0-22-gce0384b99).git diff mbedtls-4.2.0 HEAD -- library/x509_create.cis empty, so released 4.2.0 is affected.library/oid.c:990in 3.6 and the loop body is character-for-character identical to 4.x. A backport will be needed.cmake -DENABLE_PROGRAMS=On, default optionsExpected behavior
include/mbedtls/oid.h:296-297states:2.5.4294967296has an arc beyond the range ofunsigned intand is not an OID this encoder can represent, so it should be rejected withMBEDTLS_ERR_ASN1_INVALID_DATA.Actual behavior
The function returns 0 and encodes
2.5.0.cert_writeprintsokthroughout and exits 0.Steps to reproduce
Output:
Running the same command with
subject_name="2.5.0=#0C0141"produces a byte-identical certificate.Additional information
Any attribute type can be produced
Prefixing an arc with
4294967296wraps the accumulator to zero; the digits that follow then build the value normally. So4294967296+ decimal digits ofNyields arcN, at any position, in any OID. All of the following were produced withcert_writeon an unmodified default build, exit code 0:subject_namepassed on the command line2.5.4.42949672963=#0C0161CN = a(commonName)2.5.4.42949672965=#0C0161serialNumber = a2.5.4.42949672966=#0C0161C = a(countryName)2.5.4.429496729610=#0C0161O = a(organizationName)2.5.4.429496729611=#0C0161OU = a(organizationalUnitName)2.5.4.4294967296132=#0C01612.5.4.132 = a1.2.840.113549.1.9.42949672961=#0C0161emailAddress = acert_reqbehaves identically, so CSRs are affected as well:So a DN string that contains no recognisable attribute name, and no textual match for the target OID, can produce a certificate or CSR carrying any attribute type. The string-to-DER mapping is onto: every OID has an unbounded family of distinct decimal strings that encode to it.
Root cause
library/x509_create.c:288-296(4.x),library/oid.c:996-1004(3.6):The guard constrains
*num; the code uses*num * 10 + d. At*num == UINT_MAX / 10 == 429496729the guard passes,*num * 10 == 4294967290, and addingdin{6,7,8,9}crossesUINT_MAX.The important part is what happens next: after the wrap
*numis{0,1,2,3}, which is far below the guard threshold, so the loop keeps consuming digits and building a new value from the wrapped one. A string is rejected only if some prefix exceeds429496729without having wrapped first — which is why4294967300is correctly rejected but42949672966is not.Why the existing safeguards do not catch it
library/x509_create.c:406:if (component2 > (UINT_MAX - (component1 * 40)))— subtract first, then compare, which is the form the in-loop guard is missing.tests/suites/test_suite_x509write.data:336,339pin2.4294967215as accepted and2.4294967216as rejected, but2.4294967216is rejected by thecomponent2check above, not by the in-loop guard. The in-loop guard has never been covered.mbedtls_x509_string_to_names()treats a 0 return as success without re-validating the arcs.Suggested regression tests
For
tests/suites/test_suite_x509write.data, next to the existing pair:The first two come from a bounded model check of
oid_parse_numberwith ESBMC: the property was "if the function reports success, the value it produces must equal the value the digit string denotes", with the input constrained only to ASCII digits, and the counterexample was"4294967299"— accepted, but yielding3. The third covers the continue-after-wrap case, which a single-wrap check would miss.Suggested fix
library/x509_create.c:290(and the corresponding line in 3.6'slibrary/oid.c):On severity
I do not believe this is a vulnerability under the project's threat model, and I am not requesting security handling.
SECURITY.mdscopes security issues to memory corruption and undefined behaviour when parsing certificates, CSRs and CRLs, and states that Mbed TLS "must not be used to sign untrusted CSRs" and is "unsuitable for use in a Certificate Authority" — which is the only setting in which an adversary would be supplying the DN string.Reporting it as a correctness defect: the documented
\retvalcontract is not honoured, and the string-to-DER mapping is onto where the API's semantics require it to be injective. The practical consequence for anyone doing attribute-type filtering on the string form is that such a filter can be bypassed entirely, which seemed worth stating even though the setting is one the project already declines to support.Duplicate search
Searched issues and pull requests. The closest hits do not cover this defect:
mbedtls_oid_from_numeric_string(); its tests are the:336/:339pair above, which do not reach the in-loop guard.mbedtls_x509_string_to_names()mishandles repeated types" — same function family, different defect (repeated RDN types being overwritten).mbedtls_x509_set_extension(..)which leads to a segementation fault #8687 (closed) "Possible overflow inmbedtls_x509_set_extension(..)" — overflow in a different function.