It is really a pain to read this AI generated slop. It is utterly long
verbose and actually wrong.
Please stop wasting my and other peoples time and shorten it down with
your own brain.
Just to point one thing out:
> The CCR (Compact Certificate Revocation) parser is a newer addition that
CCR is not that. It stands for Canonical Cache Representation.
Also this is very similar to the -f report before and I think this is
again incorrect. MAX_ASPA_PROVIDERS is an upper bound we enforce when
building the validated payloads and it should not be used in -f mode.
On Wed, Apr 08, 2026 at 10:57:46PM -0400, Daniel Anderson wrote:
> 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);
>
--
:wq Claudio