{"repro_id":"REPRO-2026-00333","version":7,"title":"Crypt::OpenSSL::PKCS12 before 1.98 can crash with a NULL pointer dereference when `info_as_hash()` parses a crafted PKCS#12 containing a zero-length BMPSTRING attribute.","repro_type":"security","status":"published","severity":"medium","description":"Crypt::OpenSSL::PKCS12 versions before 1.98 contain a NULL pointer dereference in `print_attribute()` when parsing a crafted PKCS#12 file containing a zero-length BMPSTRING bag attribute. An attacker who can supply an untrusted PKCS12 to `info_as_hash()` can crash the Perl process.","root_cause":"# Root Cause Analysis — CVE-2026-17510\n\n## Summary\nCrypt::OpenSSL::PKCS12 before 1.98 contains a NULL pointer dereference in the\n`V_ASN1_BMPSTRING` branch of `print_attribute()` (PKCS12.xs). When\n`info_as_hash()` parses a crafted PKCS#12 file containing a zero-length\nBMPSTRING bag attribute, `Renew(*attribute, 0, char)` (Perl's\n`safesysrealloc`) frees the destination buffer and returns `NULL`; the `NULL`\nis stored back into `*attribute`, and the downstream caller runs\n`newSVpvn(attribute_value, strlen(attribute_value))`, dereferencing `NULL`\ninside `strlen()` and causing a deterministic SIGSEGV (exit status 139).\n\n## Impact\n- Package: `Crypt-OpenSSL-PKCS12` (CPAN), XS binding to OpenSSL's PKCS12 API.\n- Affected versions: all versions before 1.98 (vulnerable code confirmed at\n  commit `5934ce7fe7c4683c8d9a08edcbd0c6871a52945f`, the parent of the fix).\n- Risk: medium — remote/unauthenticated denial of service of any Perl process\n  that calls `info_as_hash()` on attacker-supplied PKCS#12 data. The `info()`\n  path is unaffected (it uses the `BIO_printf` branch and never calls `Renew`).\n\n## Impact Parity\n- Disclosed/claimed maximum impact: denial of service (process crash via NULL\n  dereference). The advisory explicitly scopes impact to DoS; no memory\n  disclosure or code execution is claimed.\n- Reproduced impact from this run: deterministic SIGSEGV (exit 139, core\n  dumped) in the real library function `info_as_hash()` on the vulnerable\n  build, twice in a row; fixed build returns the attribute as the empty\n  string and completes normally, twice in a row.\n- Parity: **full** — the claimed DoS impact was demonstrated exactly.\n- Not demonstrated: nothing beyond the claim; no code execution was claimed\n  or attempted.\n\n## Root Cause\nIn `print_attribute()` (pre-fix PKCS12.xs, line ~666):\n\n```c\nvalue = OPENSSL_uni2asc(av->value.bmpstring->data, length);\nif (*attribute != NULL) {\n  Renew(*attribute, length, char);        /* length == ASN.1 byte length */\n  strncpy(*attribute, value, length);\n}\n```\n\nFor a normal BMPSTRING this is benign because `OPENSSL_uni2asc()` returns a\nNUL-terminated ASCII string and `strncpy` zero-pads the oversized buffer. For\nan **empty** BMPSTRING (`length == 0`) it degenerates: Perl's\n`safesysrealloc` treats a zero size as free-and-return-NULL, so\n`Renew(*attribute, 0, char)` frees the buffer, `*attribute` becomes `NULL`,\n`strncpy(NULL, value, 0)` writes nothing, and the downstream\n`dump_certs_pkeys_bag` / `print_attribs` code calls\n`newSVpvn(attribute_value, strlen(attribute_value))` on the `NULL` pointer —\na deterministic NULL dereference in `strlen()`. Only `info_as_hash()` reaches\nthis branch because it passes a non-NULL hash, making `*attribute` non-NULL.\n\nFix commit: https://github.com/dsully/perl-crypt-openssl-pkcs12/commit/6cb282d8d8e8ded4859551cd2d3cfa7c6028ce48\nThe fix sizes the buffer with `strlen(value) + 1` (never zero), copies with\n`memcpy`, writes an explicit terminator, and adds a NULL check on the\n`OPENSSL_uni2asc()` return value.\n\n## Reproduction Steps\n1. `bundle/repro/reproduction_steps.sh` (self-contained; run from anywhere,\n   honors `PRUVA_ROOT`).\n2. The script:\n   - Clones `dsully/perl-crypt-openssl-pkcs12` (uses the prepared project\n     cache mirror when available, GitHub otherwise).\n   - Resolves `VULN_COMMIT = 6cb282d...^` = `5934ce7fe7c4...` and\n     `FIXED_COMMIT = 6cb282d8d8e8ded4859551cd2d3cfa7c6028ce48`, and verifies\n     the vulnerable tree contains the pre-fix `Renew(*attribute, length,\n     char)` hunk while the fixed tree contains the `strlen(value) + 1` fix.\n   - Installs the pure-Perl configure dependency `Crypt::OpenSSL::Guess` into\n     a bundle-local `INSTALL_BASE` if missing.\n   - Builds the XS module from both commits with `perl Makefile.PL && make`.\n   - Extracts the crafted fixture `certs/bmpstring-empty.p12` from the fixed\n     commit (it ships there as the regression-test fixture: a certBag whose\n     bag attribute at OID `1.2.3.4.6` is a zero-length ASN.1 BMPSTRING,\n     password `Password1`, SHA-256 MAC) and validates it with\n     `openssl pkcs12 -info`.\n   - Runs `Crypt::OpenSSL::PKCS12->new_from_file(...)->info_as_hash('Password1')`\n     twice against the vulnerable build and twice against the fixed build.\n3. Expected evidence: both vulnerable attempts die with SIGSEGV (exit 139,\n   core dumped, `INFO_AS_HASH_RETURNED` never printed); both fixed attempts\n   print `attribute 1.2.3.4.6 value=<>` and `INFO_AS_HASH_RETURNED` with\n   exit 0.\n\n## Evidence\n- `bundle/logs/reproduction_steps.log` — full script transcript, including:\n  - `[*] vuln-attempt-1 exit=139` / `Segmentation fault (core dumped)` and\n    `timeout: the monitored command dumped core`\n  - `[*] vuln-attempt-2 exit=139`\n  - `[*] fixed-attempt-1 exit=0` → `attribute 1.2.3.4.6 value=<>`,\n    `INFO_AS_HASH_RETURNED`\n  - `[*] fixed-attempt-2 exit=0` → same\n- `bundle/logs/vuln-attempt-{1,2}.log`, `bundle/logs/fixed-attempt-{1,2}.log`\n  — per-attempt output.\n- `bundle/logs/build-vuln.log`, `bundle/logs/build-fixed.log` — build logs.\n- `bundle/repro/runtime_manifest.json` — machine-readable runtime evidence\n  (`entrypoint_kind=function_call`, `target_path_reached=true`, commit and\n  digest identity).\n- Environment: Perl 5.38.2 (x86_64-linux-gnu-thread-multi), OpenSSL 3.0.13\n  (module linked against system libssl/libcrypto), gcc, Ubuntu noble.\n- A `Data::Dumper` dump of `info_as_hash()` on the fixed build confirms\n  `'bag_attributes' => { '1.2.3.4.6' => '' }` — the empty-string return the\n  advisory predicts for the patched version.\n\n## Recommendations / Next Steps\n- Upgrade to Crypt-OpenSSL-PKCS12 1.98 or later.\n- The upstream fix (size on `strlen(value) + 1`, explicit terminator, NULL\n  check on `OPENSSL_uni2asc()`) is correct and verified by this run.\n- Services accepting untrusted PKCS#12 uploads should not call\n  `info_as_hash()` on unpatched versions; sandboxing the parse in a\n  disposable process limits DoS blast radius.\n\n## Additional Notes\n- Idempotency: the script was executed twice consecutively; both runs exited\n  0 with identical verdicts. Re-runs reuse the local mirror and rebuild both\n  worktrees from scratch (`rm -rf` + fresh `git worktree add`).\n- No sanitizer was used; the crash is a product-visible native SIGSEGV from\n  the real XS library (`sanitizer_used=false`).\n- The crafted fixture is not synthesized by this run: it is the exact\n  regression fixture `certs/bmpstring-empty.p12` shipped in the upstream fix\n  commit, so the attacker input is byte-identical to what upstream used to\n  prove the bug.\n- The claim surface is `library_api` / `function_call`; the proof invokes the\n  real published library entry points (`new_from_file`, `info_as_hash`)\n  through the module's own compiled XS code, matching the claim contract.\n","cve_id":"CVE-2026-17510","cwe_id":"CWE-476 NULL Pointer Dereference","source_url":"NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-17510","package":{"name":"dsully/perl-crypt-openssl-pkcs12","ecosystem":"CPAN","affected_versions":"Versions before 1.98","fixed_version":"1.98"},"reproduced_at":"2026-08-23T15:39:12.158055+00:00","duration_secs":1067.0,"tool_calls":162,"handoffs":2,"total_cost_usd":4.137184,"agent_costs":{"claim_matcher":0.014023,"judge":0.370964,"learning_policy":0.01249,"repro":0.745156,"support":0.074536,"vuln_variant":2.920015},"cost_breakdown":{"claim_matcher":{"gpt-5.4-mini-2026-03-17":0.014023},"judge":{"gpt-5.6-sol":0.370964},"learning_policy":{"gpt-5.4-mini-2026-03-17":0.01249},"repro":{"accounts/fireworks/models/kimi-k3":0.745156},"support":{"accounts/fireworks/models/kimi-k3":0.074536},"vuln_variant":{"accounts/fireworks/models/kimi-k3":2.920015}},"quality":{"confidence":"high","idempotent_verified":false,"community_verifications":0},"evidence":{"workflow":{"profile":"known_vulnerability","schema_version":2,"stages":["support","claim_contract","repro","judge","vuln_variant"]}},"environment":{"sandbox_image":"ghcr.io/n3mes1s/pruva-sandbox@sha256:8096b2518d6022e13d68f885c3b8ded6b4fe607098b1a1ccbfb99abc004d1dc1"},"published_at":"2026-08-23T15:39:12.670612+00:00","retracted":false,"artifacts":[{"path":"bundle/repro/rca_report.md","filename":"rca_report.md","size":6804,"category":"analysis"},{"path":"bundle/repro/reproduction_steps.sh","filename":"reproduction_steps.sh","size":8874,"category":"reproduction_script"},{"path":"bundle/logs/build-fixed.log","filename":"build-fixed.log","size":1743,"category":"log"},{"path":"bundle/logs/build-vuln.log","filename":"build-vuln.log","size":1742,"category":"log"},{"path":"bundle/logs/fixed-attempt-1.log","filename":"fixed-attempt-1.log","size":51,"category":"log"},{"path":"bundle/logs/fixed-attempt-2.log","filename":"fixed-attempt-2.log","size":51,"category":"log"},{"path":"bundle/logs/reproduction_steps.log","filename":"reproduction_steps.log","size":1871,"category":"log"},{"path":"bundle/logs/vuln-attempt-1.log","filename":"vuln-attempt-1.log","size":43,"category":"log"},{"path":"bundle/logs/vuln-attempt-2.log","filename":"vuln-attempt-2.log","size":43,"category":"log"},{"path":"bundle/repro/runtime_manifest.json","filename":"runtime_manifest.json","size":1146,"category":"other"},{"path":"bundle/repro/validation_verdict.json","filename":"validation_verdict.json","size":806,"category":"other"}]}