CVE-2026-9516
Description
Cpanel::JSON::XS before 4.41 crashes when decoding UTF-8 BOM prefixed input with a throwing filter callback.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Cpanel::JSON::XS before 4.41 crashes when decoding UTF-8 BOM prefixed input with a throwing filter callback.
Vulnerability
Cpanel::JSON::XS versions prior to 4.41 for Perl are vulnerable to a denial of service. The decode_json() function advances the input scalar's string pointer past a leading 3-byte UTF-8 BOM. If a filter callback, such as filter_json_object, throws an exception during decoding, the pointer is not restored. This leaves the scalar's pointer offset into its buffer and with a shortened length. When the scalar is later freed, the allocator receives an invalid pointer, causing the interpreter to abort [1, 2].
Exploitation
An attacker can trigger this vulnerability by providing a 5-byte input string that begins with a UTF-8 BOM (EF BB BF) followed by JSON object delimiters ({}). This input is then decoded using Cpanel::JSON::XS->new->filter_json_object(sub { die "boom\n" }). When the filter_json_object callback intentionally throws an exception, the decode_json function's internal pointer restoration logic is bypassed, corrupting the input scalar [2].
Impact
Successful exploitation results in a reliable denial-of-service. The process crashes with a SIGABRT (exit code 134) when the corrupted scalar is freed. This affects any caller of decode() that uses a filter callback which can throw an exception on invalid input [1, 2].
Mitigation
The vulnerability is fixed in Cpanel::JSON::XS version 4.41, released on 2026-05-27 [1]. Users should upgrade to this version or later. No workarounds are described in the available references.
AI Insight generated on Jun 3, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
1- Range: <4.41
Patches
2dfe1b41a36caBOM-shift PV-corruption SIGABRT (CVE-2026-9516)
2 files changed · +52 −12
t/31_bom.t+48 −1 modified@@ -2,7 +2,7 @@ # # https://tools.ietf.org/html/rfc7159#section-8.1 # JSON text SHALL be encoded in UTF-8, UTF-16, or UTF-32. -use Test::More ($] >= 5.008) ? (tests => 9) : (skip_all => "needs 5.8");; +use Test::More ($] >= 5.008) ? (tests => 9 + 6) : (skip_all => "needs 5.8");; use Cpanel::JSON::XS; use Encode; # Currently required for <5.20 use charnames qw(:short); @@ -41,3 +41,50 @@ ok(my $as_json = eval { ok(eval { $j->decode($as_json) }, 'can decode again'); ok(eval { $j->decode("\x{feff}" . $as_json) }, 'can decode with BOM'); ok(eval { $j->decode($as_json) }, 'can decode original'); + +# Tests by Paul Johnson: + +# Assert the caller's input SV is preserved bit-for-bit across a decode +# call whose filter callback throws, for each of the three croak-reachable +# callback sites (filter_json_object, filter_json_single_key_object, and +# allow_tags + THAW), with a leading UTF-8 BOM on the input. + +my $bom = "\xef\xbb\xbf"; + +sub assert_preserved { + my ($name, $payload, $setup) = @_; + my $original = $bom . $payload; + my $s = $bom . $payload; + my $j = Cpanel::JSON::XS->new; + $setup->($j); + eval { $j->decode ($s) }; + ok ($@, "$name: callback threw"); + # The BOM-decode path legitimately sets SvUTF8 on the caller's SV. + # We care about the underlying byte sequence (no SvPVX shift), not + # the flag, so clear SvUTF8 on a copy and compare raw bytes. + my $copy = $s; + Encode::_utf8_off ($copy); + is ($copy, $original, "$name: SV bytes preserved across throw"); +} + +assert_preserved ( + "filter_json_object", + '{}', + sub { $_[0]->filter_json_object (sub { die "boom\n" }) }, +); + +assert_preserved ( + "filter_json_single_key_object", + '{"k":1}', + sub { $_[0]->filter_json_single_key_object (k => sub { die "boom\n" }) }, +); + +{ + package BomFilterCorruption::Thaw; + sub THAW { die "boom\n" } +} +assert_preserved ( + "allow_tags THAW", + '("BomFilterCorruption::Thaw")[]', + sub { $_[0]->allow_tags (1) }, +);
XS.xs+4 −11 modified@@ -4502,8 +4502,6 @@ decode_json (pTHX_ SV *string, JSON *json, STRLEN *offset_return, SV *typesv) converted = 1 + (json->flags & F_UTF8); json->flags |= F_UTF8; offset = 3; - SvPV_set(string, SvPVX_mutable (string) + 3); - SvCUR_set(string, len - 3); SvUTF8_on(string); /* omitting the endian name will skip the BOM in the result */ } else if (len >= 4 && memEQc(s, UTF32BOM)) { @@ -4538,7 +4536,7 @@ decode_json (pTHX_ SV *string, JSON *json, STRLEN *offset_return, SV *typesv) SvGROW (string, SvCUR (string) + 1); dec.json = *json; - dec.cur = SvPVX (string); + dec.cur = SvPVX (string) + offset; dec.end = SvEND (string); dec.err = 0; dec.depth = 0; @@ -4552,10 +4550,11 @@ decode_json (pTHX_ SV *string, JSON *json, STRLEN *offset_return, SV *typesv) sv = decode_sv (aTHX_ &dec, typesv); if (offset_return) { - if (dec.cur < SvPVX (string) || dec.cur > SvEND (string)) + char *base = SvPVX (string) + offset; + if (dec.cur < base || dec.cur > SvEND (string)) *offset_return = 0; else - *offset_return = dec.cur - SvPVX (string); + *offset_return = dec.cur - base; } if (!(offset_return || !sv)) @@ -4570,12 +4569,6 @@ decode_json (pTHX_ SV *string, JSON *json, STRLEN *offset_return, SV *typesv) sv = NULL; } } - /* restore old utf8 string with BOM */ - if (UNLIKELY(offset)) { - SvPV_set(string, SvPVX_mutable (string) - offset); - SvCUR_set(string, len); - } - if (!sv) { SV *uni = sv_newmortal ();
f0f4551af12aMerge 2899bb28de16dfbadf5cfc19f6015cb1a46d2918 into bd714d32207c46afe7146d7bb4d02667fe48767e
Vulnerability mechanics
Root cause
"The JSON decoder fails to restore the input string's pointer and length after an exception during a filter callback, leading to memory corruption."
Attack vector
An attacker can trigger this vulnerability by providing a UTF-8 BOM prefixed JSON input to the `decode()` function. If a filter callback, such as `filter_json_object`, `filter_json_single_key_object`, or `THAW` under `allow_tags`, throws an exception during processing, the input string's internal pointer and length become corrupted. This corruption is only detected when the corrupted string is later freed, causing the interpreter to abort [ref_id=2].
Affected code
The vulnerability resides in the `decode_json` function within `XS.xs`. Specifically, the code path that handles a leading UTF-8 BOM modifies the input string's pointer and length. If an exception occurs in callback sites like `filter_json_single_key_object`, `filter_json_object`, or `THAW` under `allow_tags` before the string is restored, it leads to corruption [ref_id=2].
What the fix does
The patch removes the mutation of the caller's input SV by `decode_json()` when handling a UTF-8 BOM. Instead of directly modifying the string's pointer and length, the offset is now managed internally. This ensures that even if a filter callback throws an exception, the original string's state remains intact, preventing memory corruption and subsequent crashes when the string is freed [patch_id=4552432].
Preconditions
- inputThe input JSON must be prefixed with a UTF-8 BOM (EF BB BF).
- configA filter callback must be configured that can throw an exception during JSON decoding.
Reproduction
```perl my $s = pack("H*", "efbbbf7b7d"); my $j = Cpanel::JSON::XS->new->filter_json_object(sub { die "boom\n" }); eval { $j->decode($s) }; undef $s ```
Generated on Jun 3, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
2News mentions
0No linked articles in our index yet.