VYPR
Unrated severityNVD Advisory· Published May 31, 2026

CVE-2026-8796

CVE-2026-8796

Description

Sereal::Decoder before 5.005 has a heap out-of-bounds read via crafted COPY tags, leading to potential info disclosure.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Sereal::Decoder before 5.005 has a heap out-of-bounds read via crafted COPY tags, leading to potential info disclosure.

Vulnerability

A heap out-of-bounds read vulnerability exists in Sereal::Decoder versions before 5.005. The flaw resides in srl_decoder.c in the functions srl_read_object() and srl_read_hash(). When processing a COPY tag—a back-reference whose target byte is re-decoded as a fresh tag—the decoder fails to ensure that the target offset precedes the COPY tag's own offset. If the target byte matches the SHORT_BINARY pattern (where the low bits encode the length of an inline string), the decoder reads up to 31 bytes beyond the end of the input buffer. An attacker can craft a Sereal blob with a COPY offset that lands inside a previously decoded value rather than on a tag boundary, planting a byte that the decoder misinterprets as a SHORT_BINARY tag [1].

Exploitation

An attacker needs the ability to supply a crafted Sereal blob to the decoder. No authentication or special privileges are required if the application processes untrusted Sereal input. The attacker constructs a blob containing a COPY tag whose offset points to a location within a prior decoded value. The byte at that offset is then interpreted as a SHORT_BINARY tag, and the decoder reads the subsequent bytes (up to 31) as a class name (in the OBJECT path) or hash key (in the HASH path). By controlling those bytes, the attacker can force the decoder to read heap memory beyond the input buffer.

Impact

Successful exploitation results in a heap out-of-bounds read, potentially disclosing sensitive heap memory contents such as other data or secrets. The vulnerability does not directly allow remote code execution or file write, but information leakage can aid further attacks. The CVSS score and severity have not been disclosed in the available references.

Mitigation

The vulnerability is fixed in version 5.005 of Sereal::Decoder, released on or around 2026-05-19 [1]. Users should upgrade to version 5.005 or later. No workaround is available; applications must update the affected library. The fixed version also adds documentation and new tests for the issue.

AI Insight generated on May 31, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

2
  • Sereal/Serealreferences2 versions
    (expand)+ 1 more
    • (no CPE)
    • (no CPE)range: <5.005

Patches

1
303a2c69cdba

Release v5.005 - Security release, prevent buffer overrun reads from COPY tags

https://github.com/sereal/serealYves OrtonMay 19, 2026via nvd-ref
15 files changed · +136 26
  • Perl/Decoder/Changes+5 0 modified
    @@ -5,6 +5,11 @@ Revision history for Perl extension Sereal-Decoder
     *          of the decoder before upgrading to version 5 of the *
     *          encoder!                                            *
     ****************************************************************
    +5.005 
    +    * Update spec to document changes from version 5
    +    * Security fixes - make sure that COPY tags cannot be used
    +      to read past end of buffer.
    +
     5.004_001
         * Update Zstd to 1.5.7, and other changes.
         * Thanks to Marco Fontani, Ricardo Signes, and James Rouzier
    
  • Perl/Decoder/lib/Sereal/Decoder/Constants.pm+1 1 modified
    @@ -4,7 +4,7 @@ use warnings;
     require Exporter;
     our @ISA= qw(Exporter);
     
    -our $VERSION= '5.004_001';
    +our $VERSION= '5.005';
     
     our ( @EXPORT_OK, %DEFINE, %TAG_INFO_HASH, @TAG_INFO_ARRAY );
     
    
  • Perl/Decoder/lib/Sereal/Decoder.pm+1 1 modified
    @@ -5,7 +5,7 @@ use warnings;
     use Carp qw/croak/;
     use XSLoader;
     
    -our $VERSION= '5.004_001';
    +our $VERSION= '5.005';
     our $XS_VERSION= $VERSION; $VERSION= eval $VERSION;
     
     use Exporter 'import';
    
  • Perl/Decoder/MANIFEST+1 0 modified
    @@ -136,6 +136,7 @@ t/900_regr_issue_15.t
     t/901_regr_segv.t
     t/902_bad_input.t
     t/903_reentrancy.t
    +t/903_regr_oob_copy_shortbinary.t
     t/data/corpus
     t/lib/Sereal/BulkTest.pm
     t/lib/Sereal/TestSet.pm
    
  • Perl/Decoder/srl_decoder.c+26 14 modified
    @@ -167,6 +167,8 @@ SRL_STATIC_INLINE SV *srl_read_extend(pTHX_ srl_decoder_t *dec, SV* into);
             SvROK_on(into);                             \
         } STMT_END
     
    +#define COPY_OFFSET_ERROR_FMT_PREFIX "Corrupted COPY tag, offset %d to %d must precede the tags own offset"
    +
     STATIC void
     srl_ptable_debug_callback(PTABLE_ENTRY_t *e)
     {
    @@ -1237,26 +1239,25 @@ srl_read_hash(pTHX_ srl_decoder_t *dec, SV* into, U8 tag) {
                 flags= HVhek_UTF8;
     #endif
             } else if (tag == SRL_HDR_COPY) {
    +            const U8 *copytag_start = dec->buf.pos - 1;
                 UV ofs= srl_read_varint_uv_offset(aTHX_ dec->pbuf, " while reading COPY tag");
                 from= dec->buf.body_pos + ofs;
                 tag= *from++;
    -            /* note we do NOT validate these items, as we have alread read them
    -             * and if they were a problem we would not be here to process them! */
                 if (IS_SRL_HDR_SHORT_BINARY(tag)) {
                     key_len= (KEYLENTYPE)SRL_HDR_SHORT_BINARY_LEN_FROM_TAG(tag);
                 }
                 else
                 if (tag == SRL_HDR_BINARY) {
                     key_len = (KEYLENTYPE)S_read_varint_uv_length_char_ptr(
                         aTHX_ &from, dec->buf.end,
    -                    " while reading (byte) string length (via COPY)"
    +                    " while reading binary key via COPY"
                     );
                 }
                 else
                 if (tag == SRL_HDR_STR_UTF8) {
                     key_len = (KEYLENTYPE)S_read_varint_uv_length_char_ptr(
                         aTHX_ &from, dec->buf.end,
    -                    " while reading UTF8-encoded string length (via COPY)"
    +                    " while reading UTF8-encoded key via COPY"
                     );
     #ifdef OLDHASH
                     key_len= -key_len;
    @@ -1267,6 +1268,10 @@ srl_read_hash(pTHX_ srl_decoder_t *dec, SV* into, U8 tag) {
                 else {
                     SRL_RDR_ERROR_BAD_COPY(dec->pbuf, SRL_HDR_HASH);
                 }
    +            if (expect_false(from + key_len > copytag_start)) {
    +                SRL_RDR_ERRORf2(dec->pbuf,
    +                    COPY_OFFSET_ERROR_FMT_PREFIX " while reading key",(int)ofs,(int)key_len);
    +            }
             } else {
                 SRL_RDR_ERROR_UNEXPECTED(dec->pbuf, tag, "a stringish type");
             }
    @@ -1550,11 +1555,12 @@ srl_read_object(pTHX_ srl_decoder_t *dec, SV* into, U8 obj_tag, int read_class_n
         }
         else
         if (tag == SRL_HDR_COPY) {
    +        const U8 *copytag_start = dec->buf.pos - 1;
             ofs= srl_read_varint_uv_offset(aTHX_ dec->pbuf, " while reading COPY class name");
             storepos= ofs;
             /* if this string was seen before as part of a classname then we expect
              * a stash available below. However it might have been serialized as a key
    -         * or something like that, which would mean we dont have an entry in ref_stashes
    +         * or something like that, which would mean we don't have an entry in ref_stashes
              * anymore. So first we check if we have a stash. If we do, then we can avoid
              * some work. */
             if (expect_true( dec->ref_stashes != NULL )) {
    @@ -1565,23 +1571,21 @@ srl_read_object(pTHX_ srl_decoder_t *dec, SV* into, U8 obj_tag, int read_class_n
             if (!class_stash) {
                 from= dec->buf.body_pos + ofs;
                 tag= *from++;
    -            /* Note we do NOT validate these items, as we have already read them
    -             * and if they were a problem we would not be here to process them! */
                 if (IS_SRL_HDR_SHORT_BINARY(tag)) {
                     key_len= SRL_HDR_SHORT_BINARY_LEN_FROM_TAG(tag);
                 }
                 else
                 if (tag == SRL_HDR_BINARY) {
                     key_len = (KEYLENTYPE)S_read_varint_uv_length_char_ptr(
                         aTHX_ &from, dec->buf.end,
    -                    " while reading (byte) length for class name (via COPY)"
    +                    " while reading class name via COPY"
                     );
                 }
                 else
                 if (tag == SRL_HDR_STR_UTF8) {
                     key_len = (KEYLENTYPE)S_read_varint_uv_length_char_ptr(
                         aTHX_ &from, dec->buf.end,
    -                    " while reading UTF8 string length for class name (via COPY)"
    +                    " while reading UTF8 class name via COPY"
                     );
                     flags = flags | SVf_UTF8;
                     if (!is_utf8_string(from, key_len)) {
    @@ -1591,6 +1595,10 @@ srl_read_object(pTHX_ srl_decoder_t *dec, SV* into, U8 obj_tag, int read_class_n
                 else {
                     SRL_RDR_ERROR_BAD_COPY(dec->pbuf, SRL_HDR_OBJECT);
                 }
    +            if (expect_false(from + key_len > copytag_start)) {
    +                SRL_RDR_ERRORf2(dec->pbuf,
    +                    COPY_OFFSET_ERROR_FMT_PREFIX " while reading classname",(int)ofs,(int)ofs+key_len);
    +            }
             }
         } else {
             SRL_RDR_ERROR_UNEXPECTED(dec->pbuf, tag, "a class name");
    @@ -1878,15 +1886,19 @@ srl_read_extend(pTHX_ srl_decoder_t *dec, SV* into)
     SRL_STATIC_INLINE void
     srl_read_copy(pTHX_ srl_decoder_t *dec, SV* into)
     {
    -    UV item= srl_read_varint_uv_offset(aTHX_ dec->pbuf, " while reading COPY tag");
    +    const U8 *copytag_start = dec->buf.pos - 1;
    +    UV ofs= srl_read_varint_uv_offset(aTHX_ dec->pbuf, " while reading COPY tag");
    +    const U8 *target_start = dec->buf.body_pos + ofs;
    +
         if (expect_false( dec->save_pos )) {
    -        SRL_RDR_ERRORf1(dec->pbuf, "COPY(%d) called during parse", (int)item);
    +        SRL_RDR_ERRORf2(dec->pbuf, "nested COPY at offset %d and %d",
    +                (int)(dec->save_pos - dec->buf.body_pos), (int)ofs );
         }
    -    if (expect_false( (IV)item > dec->buf.end - dec->buf.start )) {
    -        SRL_RDR_ERRORf1(dec->pbuf, "COPY(%d) points out of packet", (int)item);
    +    if (expect_false( target_start > copytag_start )) {
    +        SRL_RDR_ERRORf1(dec->pbuf, "COPY target %d must preceed its own offset", (int)ofs);
         }
         dec->save_pos= dec->buf.pos;
    -    dec->buf.pos= dec->buf.body_pos + item;
    +    dec->buf.pos= target_start;
         srl_read_single_value(aTHX_ dec, into, NULL);
         dec->buf.pos= dec->save_pos;
         dec->save_pos= 0;
    
  • Perl/Decoder/t/903_regr_oob_copy_shortbinary.t+66 0 added
    @@ -0,0 +1,66 @@
    +#!perl
    +use strict;
    +use warnings;
    +use File::Spec;
    +use lib File::Spec->catdir(qw(t lib));
    +
    +BEGIN {
    +    lib->import('lib')
    +        if !-d 't';
    +}
    +
    +use Test::More tests => 2;
    +use Sereal::Decoder qw(decode_sereal);
    +
    +my $base = "Corrupted COPY tag, offset %d to %d must precede the tags own offset";
    +$base=~s/%d/\\d+/g;
    +
    +# Regression coverage for the CWE-125 heap OOB read via a COPY tag whose
    +# offset points one byte inside a previously-decoded BINARY's content,
    +# where the planted byte matches the SHORT_BINARY tag pattern
    +# (0x60..0x7F). srl_read_object (class name) and srl_read_hash (key)
    +# both had unchecked SHORT_BINARY-inline COPY branches; the fix bounds
    +# `from + key_len` against `dec->buf.end` and croaks on overrun.
    +
    +sub build_packet {
    +    my ($container_bytes) = @_;
    +    my $header = "=\xF3rl" . chr(0x05) . chr(0x00);
    +    # 30-byte BINARY whose final byte is 0x7F (SHORT_BINARY-len-31
    +    # forgery). The COPY offset 34 lands on this byte: from there the
    +    # decoder reads 31 bytes for the inline SHORT_BINARY length,
    +    # overrunning buf.end by 26 bytes without the fix.
    +    my $binary_content = (chr(0x42) x 29) . chr(0x7F);
    +    my $body = chr(0x2B) . chr(0x02)        # ARRAY count=2
    +             . chr(0x26) . chr(0x1E)        # BINARY length=30
    +             . $binary_content
    +             . $container_bytes;
    +    return $header . $body;
    +}
    +
    +# OBJECT variant: OBJECT + COPY(34) + REFN + UNDEF. The COPY-target
    +# byte is decoded as the OBJECT's class name.
    +{
    +    my $packet = build_packet(
    +        chr(0x2C) . chr(0x2F) . chr(34) . chr(0x28) . chr(0x25)
    +    );
    +    eval { decode_sereal($packet) };
    +    like(
    +        $@,
    +        qr/$base while reading classname/,
    +        "srl_read_object SHORT_BINARY-COPY OOB rejected with bounds error"
    +    );
    +}
    +
    +# HASH variant: HASH(1) + COPY(34) + UNDEF. The COPY-target byte is
    +# decoded as the hash key.
    +{
    +    my $packet = build_packet(
    +        chr(0x2A) . chr(0x01) . chr(0x2F) . chr(34) . chr(0x25)
    +    );
    +    eval { decode_sereal($packet) };
    +    like(
    +        $@,
    +        qr/$base while reading key/,
    +        "srl_read_hash SHORT_BINARY-COPY OOB rejected with bounds error"
    +    );
    +}
    
  • Perl/Encoder/Changes+4 0 modified
    @@ -5,6 +5,10 @@ Revision history for Perl extension Sereal-Encoder
     *          of the decoder before upgrading to version 5 of the *
     *          encoder!                                            *
     ****************************************************************
    +5.005 
    +    * Update spec to document changes from version 5
    +    * Security fixes in decoder.
    +
     5.004_001
         * Update Zstd to 1.5.7, and other changes.
         * Thanks to Marco Fontani, Ricardo Signes, and James Rouzier
    
  • Perl/Encoder/lib/Sereal/Encoder/Constants.pm+1 1 modified
    @@ -4,7 +4,7 @@ use warnings;
     require Exporter;
     our @ISA= qw(Exporter);
     
    -our $VERSION= '5.004_001';
    +our $VERSION= '5.005';
     
     our ( @EXPORT_OK, %DEFINE, %TAG_INFO_HASH, @TAG_INFO_ARRAY );
     
    
  • Perl/Encoder/lib/Sereal/Encoder.pm+1 1 modified
    @@ -5,7 +5,7 @@ use warnings;
     use Carp qw/croak/;
     use XSLoader;
     
    -our $VERSION= '5.004_001';
    +our $VERSION= '5.005';
     our $XS_VERSION= $VERSION; $VERSION= eval $VERSION;
     
     # Make sure to keep these constants in sync with the C code in srl_encoder.c.
    
  • Perl/Encoder/Makefile.PL+1 1 modified
    @@ -6,7 +6,7 @@ use warnings;
     use ExtUtils::MakeMaker;
     use Config;
     
    -our $VERSION= '5.004_001'; # what version are we and what version of Decoder do we need.
    +our $VERSION= '5.005'; # what version are we and what version of Decoder do we need.
     $VERSION = eval $VERSION or die "WTF: $VERSION: $@"; # deal with underbars
     
     my $shared_dir= "../shared";
    
  • Perl/Sereal/lib/Sereal.pm+3 3 modified
    @@ -2,17 +2,17 @@ package Sereal;
     use 5.008;
     use strict;
     use warnings;
    -our $VERSION= '5.004_001';
    +our $VERSION= '5.005';
     our $XS_VERSION= $VERSION; $VERSION= eval $VERSION;
    -use Sereal::Encoder 5.004_001 qw(
    +use Sereal::Encoder 5.005 qw(
         encode_sereal
         sereal_encode_with_object
         SRL_UNCOMPRESSED
         SRL_SNAPPY
         SRL_ZLIB
         SRL_ZSTD
     );
    -use Sereal::Decoder 5.004_001 qw(
    +use Sereal::Decoder 5.005 qw(
         decode_sereal
         looks_like_sereal
         decode_sereal_with_header_data
    
  • Perl/Sereal/Makefile.PL+1 1 modified
    @@ -4,7 +4,7 @@ use warnings;
     
     use ExtUtils::MakeMaker;
     use Cwd;
    -our $VERSION= '5.004_001';
    +our $VERSION= '5.005';
     $VERSION = eval $VERSION or die "WTF: $VERSION: $@"; # deal with underbars
     
     my $shared_dir= "../shared";
    
  • README.pod+1 1 modified
    @@ -133,6 +133,6 @@ would like to express their gratitude.
     
     Copyright (C) 2012, 2013, 2014 by Steffen Mueller
     
    -Copyright (C) 2012, 2013, 2014 by Yves Orton
    +Copyright (C) 2012, 2013, 2014, 2026 by Yves Orton
     
     =cut
    
  • SECURITY.md+17 0 added
    @@ -0,0 +1,17 @@
    +# Security Policy
    +
    +## Supported Versions
    +
    +Always use the latest version of the package. Always update
    +the decoder first, and then the encoder. 
    +
    +## Reporting a Vulnerability
    +
    +Report vulnerabilities to the issue tracker at github:
    +https://github.com/Sereal/Sereal/issues
    +
    +We try to respond to any reasonable security reports in a
    +speedy fashion, but make no guarantees.
    +
    +We very much appreciate patches which either fix or 
    +illustrate any security issues in the project.
    
  • sereal_spec.pod+7 2 modified
    @@ -12,7 +12,7 @@ This document describes the format and encoding of a Sereal data packet.
     
     =head1 VERSION
     
    -This is the Sereal specification version 4.00.
    +This is the Sereal specification version 5.00.
     
     The integer part of the document version corresponds to
     the Sereal protocol version. For details on incompatible changes between
    @@ -83,7 +83,7 @@ A single byte, of which the high 4 bits are used to represent the "type"
     of the document, and the low 4 bits used to represent the version of the
     Sereal protocol the document complies with.
     
    -Up until now there have been versions 1, 2, 3 and 4 of the Sereal protocol.
    +Up until now there have been versions 1, 2, 3 and 4, 5 of the Sereal protocol.
     So the low four bits will be one of those values in little-endian.
     
     Currently only five types are defined:
    @@ -519,6 +519,11 @@ accordingly, otherwise they are free to treat CANONICAL_UNDEF the same as UNDEF.
     
     =head1 PROTOCOL CHANGES
     
    +=head2 Protocol Version 5
    +
    +Add, the NO and YES tags to represent perl 5.36 booleans (basically JSON true/false)
    +as well as FLOAT_128.
    +
     =head2 Protocol Version 4
     
     "zstd" compression (document type 4) support is added. As detailed above, its
    

Vulnerability mechanics

Root cause

"Missing bounds validation when a COPY tag's offset points into previously decoded BINARY content and the byte at that offset is interpreted as a SHORT_BINARY tag, allowing the decoder to read up to 31 bytes past the end of the input buffer."

Attack vector

An attacker crafts a Sereal packet containing a BINARY whose final byte falls in the range `0x60..0x7F` (the SHORT_BINARY tag pattern). A subsequent COPY tag is given an offset that points to that planted byte. When the decoder processes the COPY in `srl_read_object()` or `srl_read_hash()`, it re-decodes the target byte as a SHORT_BINARY tag and reads up to 31 bytes from the heap past the end of the input buffer as a class name or hash key. No authentication is required; the attacker only needs to supply the malicious packet to a Perl application using `Sereal::Decoder` before version 5.005.

Affected code

The vulnerability resides in `srl_decoder.c` within the functions `srl_read_object()` and `srl_read_hash()`. Both functions process a COPY tag whose offset can point into the content of a previously decoded BINARY, where the byte at that offset is interpreted as a SHORT_BINARY tag. The decoder then reads up to 31 bytes from that position without verifying that the read stays within the input buffer, leading to a heap out-of-bounds read. The patch also tightens bounds checking in `srl_read_copy()` itself.

What the fix does

The patch adds a bounds check in both `srl_read_hash()` and `srl_read_object()` after the COPY tag's target byte is decoded. It computes `from + key_len` and compares it against `copytag_start` (the position of the COPY tag itself). If the computed end of the inline string would reach or exceed the COPY tag's own offset, the decoder croaks with a descriptive error instead of reading past the buffer. The same principle is applied to `srl_read_copy()` by comparing `target_start` against `copytag_start`. The old comment `/* note we do NOT validate these items … */` is removed because the fix now performs the validation that was missing.

Preconditions

  • inputThe attacker must supply a crafted Sereal packet that includes a BINARY with a final byte in the SHORT_BINARY tag range (0x60..0x7F) and a COPY tag whose offset points to that byte.
  • configThe target application must use Sereal::Decoder version 5.004_001 or earlier to decode the malicious packet.

Generated on May 31, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

2

News mentions

0

No linked articles in our index yet.