Skip to content

mbedtls_oid_from_numeric_string() accepts wrapping numeric strings, silently encoding a different OID #10910

Description

@LukeW1999

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:

subject=2.5.0=A

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:

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions