CVE-2020-28491 (Java, published 2021-02-18T16:15Z)
Description: value: This affects the package com.fasterxml.jackson.dataformat:jackson-dataformat-cbor from 0 and before 2.11.4, from 2.12.0-rc1 and before 2.12.1. Unchecked allocation of byte buffer can cause a java.lang.OutOfMemoryError exception.
Severity: MEDIUM (CVSS v3 base score: 7.5)
Weakness: CWE-770 \u2014 Allocation of Resources Without Limits or Throttling
The product allocates a reusable resource or group of resources on behalf of an actor without imposing any restrictions on the size or number of resources that can be allocated, in violation of the intended security policy for that actor.
Repository: https://github.com/FasterXML/jackson-dataformats-binary
Files changed:
cbor/src/main/java/com/fasterxml/jackson/dataformat/cbor/CBORGenerator.java
cbor/src/main/java/com/fasterxml/jackson/dataformat/cbor/CBORParser.java
cbor/src/test/java/com/fasterxml/jackson/dataformat/cbor/BrokenLongBinary186Test.java
release-notes/CREDITS-2.x
release-notes/VERSION-2.x
Commit message: Fix eager allocation aspect of #186


Vulnerable code:
     * when writing defined-length Arrays, Objects; marker {@link #INDEFINITE_LENGTH}
    private int _readAndWriteBytes(OutputStream out, int total) throws IOException
                loadMoreGuaranteed();
    
        if (len >= 0) {
            byte[] b = new byte[len];
            if (_inputPtr >= _inputEnd) {
                loadMoreGuaranteed();
            int ptr = 0;
            while (true) {
                int toAdd = Math.min(len, _inputEnd - _inputPtr);
                System.arraycopy(_inputBuffer, _inputPtr, b, ptr, toAdd);
                _inputPtr += toAdd;
                ptr += toAdd;
                len -= toAdd;
                if (len <= 0) {
                    return b;
                }
                loadMoreGuaranteed();
            len = _decodeExplicitLength(ch & 0x1F);
                    loadMoreGuaranteed();
    
            /* 08-Sep-2014, tatu: As per [Issue#5], there are codecs
             *   (f.ex. Perl module "CBOR::XS") that use Binary data...
             */
    protected final boolean loadMore() throws IOException
    protected final void loadMoreGuaranteed() throws IOException {
package com.fasterxml.jackson.dataformat.cbor.failing;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.cbor.CBORConstants;
import com.fasterxml.jackson.dataformat.cbor.CBORTestBase;

// Mostly for [dataformats-binary#186]: corrupt encoding indicating humongous payload
public class BrokenLongBinary186Test extends CBORTestBase
{
    private final ObjectMapper MAPPER = cborMapper();

    // [dataformats-binary#186]
    public void testCorruptVeryLongBinary() throws Exception
    {
        // Let's do 999,999,999 bytes to likely trigger failure
        final int LONG_LEN = 999_999_999;
        byte[] DOC = new byte[] {
                        (byte) (CBORConstants.PREFIX_TYPE_BYTES | CBORConstants.SUFFIX_UINT32_ELEMENTS),
                        (byte) (LONG_LEN >> 24),
                        (byte) (LONG_LEN >> 16),
                        (byte) (LONG_LEN >> 8),
                        (byte) LONG_LEN,
                        // but only include 2 bytes
                        0, 0
        };
        JsonParser p = MAPPER.createParser(DOC);
        assertEquals(JsonToken.VALUE_EMBEDDED_OBJECT, p.nextToken());
        try {
            p.getBinaryValue();
            fail("Should fail");
        } catch (JsonProcessingException e) {
//            e.printStackTrace();

            // 01-Dec-2020, tatu: Need to decide what kind of exception should be
            //    produced...
            verifyException(e, "foobar");
        }
    }
}
* Reported #185: Internal parsing of tagged arrays can lead to stack overflow
#219: Cache record names to avoid hitting class loader

Fixed code:
     * when writing defined-length Arrays, Objects; marker {code INDEFINITE_LENGTH}
    // 2.11.4: [dataformats-binary#186] Avoid OOME/DoS for bigger binary;
    //  read only up to 250k
    protected final static int LONGEST_NON_CHUNKED_BINARY = 250_000;

    private int _readAndWriteBytes(OutputStream out, final int total) throws IOException
                if (!loadMore()) {
                    _reportIncompleteBinaryRead(total, total-left);
                }

    /**
     * Helper called to complete reading of binary data ("byte string") in
     * case contents are needed.
     */
        // Chunked?
        if (len <= 0) {
            return _finishChunkedBytes();
        }
        // Non-chunked, contiguous
        if (len > LONGEST_NON_CHUNKED_BINARY) {
            // [dataformats-binary#186]: avoid immediate allocation for longest
            return _finishLongContiguousBytes(len);
        }

        final byte[] b = new byte[len];
        final int expLen = len;
        if (_inputPtr >= _inputEnd) {
            if (!loadMore()) {
                _reportIncompleteBinaryRead(expLen, 0);
        }

        int ptr = 0;
        while (true) {
            int toAdd = Math.min(len, _inputEnd - _inputPtr);
            System.arraycopy(_inputBuffer, _inputPtr, b, ptr, toAdd);
            _inputPtr += toAdd;
            ptr += toAdd;
            len -= toAdd;
            if (len <= 0) {
                return b;
            }
            if (!loadMore()) {
                _reportIncompleteBinaryRead(expLen, ptr);
    }
    // @since 2.12
    protected byte[] _finishChunkedBytes() throws IOException
    {
            int len = _decodeExplicitLength(ch & 0x1F);
            final int chunkLen = len;
                    if (!loadMore()) {
                        _reportIncompleteBinaryRead(chunkLen, chunkLen-len);
                    }

    // @since 2.12
    protected byte[] _finishLongContiguousBytes(final int expLen) throws IOException
    {
        int left = expLen;

        // 04-Dec-2020, tatu: Let's NOT use recycled instance since we have much
        //   longer content and there is likely less benefit of trying to recycle
        //   segments
        try (final ByteArrayBuilder bb = new ByteArrayBuilder(LONGEST_NON_CHUNKED_BINARY >> 1)) {
            while (left > 0) {
                int avail = _inputEnd - _inputPtr;
                if (avail <= 0) {
                    if (!loadMore()) {
                        _reportIncompleteBinaryRead(expLen, expLen-left);
                    }
                    avail = _inputEnd - _inputPtr;
                }
                int count = Math.min(avail, left);
                bb.write(_inputBuffer, _inputPtr, count);
                _inputPtr += count;
                left -= count;        
            }
            return bb.toByteArray();
        }
    }

            // 08-Sep-2014, tatu: As per [Issue#5], there are codecs
            //   (f.ex. Perl module "CBOR::XS") that use Binary data...
    protected boolean loadMore() throws IOException
    protected void loadMoreGuaranteed() throws IOException {
    // @since 2.12
    protected void _reportIncompleteBinaryRead(int expLen, int actLen) throws IOException
    {
        _reportInvalidEOF(String.format(" for Binary value: expected %d bytes, only found %d",
                expLen, actLen), _currToken);
    }

package com.fasterxml.jackson.dataformat.cbor;

import java.io.ByteArrayOutputStream;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.JsonToken;

import com.fasterxml.jackson.databind.ObjectMapper;

// Mostly for [dataformats-binary#186]: corrupt encoding indicating humongous payload
public class BrokenLongBinary186Test extends CBORTestBase
{
    private final ObjectMapper MAPPER = cborMapper();

    /*
    /**********************************************************************
    /* First regular, read-it-all access, from non-chunked
    /**********************************************************************
     */

    // [dataformats-binary#186]
    public void testCorruptVeryLongBinary() throws Exception {
        // Let's do about 2 GB to likely trigger failure
        _testCorruptLong(1_999_999_999, 95000);
    }

    // [dataformats-binary#186]
    public void testCorruptQuiteLongBinary() throws Exception {
        // Value below limit for chunked handling
        _testCorruptLong(CBORParser.LONGEST_NON_CHUNKED_BINARY >> 1, 37);
    }

    private void _testCorruptLong(int allegedLength, int actualIncluded) throws Exception
    {
        JsonParser p = MAPPER.createParser(_createBrokenDoc(allegedLength, actualIncluded));
        assertEquals(JsonToken.VALUE_EMBEDDED_OBJECT, p.nextToken());
        try {
            p.getBinaryValue();
            fail("Should fail");
        } catch (JsonProcessingException e) {
            verifyException(e, "Unexpected end-of-input for Binary value");
            verifyException(e, "expected "+allegedLength+" bytes, only found "+actualIncluded);
        }
    }

    /*
    /**********************************************************************
    /* And then "streaming" access
    /**********************************************************************
     */

    // [dataformats-binary#186]
    public void testQuiteLongStreaming() throws Exception
    {
        // Can try bit shorter here, like 500 megs
        final int allegedLength = 500_000_000;
        
        JsonParser p = MAPPER.createParser(_createBrokenDoc(allegedLength, 72000));
        assertEquals(JsonToken.VALUE_EMBEDDED_OBJECT, p.nextToken());
        try {
            ByteArrayOutputStream bytes = new ByteArrayOutputStream();
            p.readBinaryValue(bytes);
            fail("Should fail");
        } catch (JsonProcessingException e) {
            verifyException(e, "Unexpected end-of-input for Binary value");
            verifyException(e, "expected "+allegedLength+" bytes, only found 72000");
        }
    }

    private byte[] _createBrokenDoc(int allegedLength, int actualIncluded) throws Exception
    {
        ByteArrayOutputStream bytes = new ByteArrayOutputStream();

        if (allegedLength > 0xFFFF) {
            bytes.write((byte) (CBORConstants.PREFIX_TYPE_BYTES | CBORConstants.SUFFIX_UINT32_ELEMENTS));
            bytes.write((byte) (allegedLength >> 24));
            bytes.write((byte) (allegedLength >> 16));
            bytes.write((byte) (allegedLength >> 8));
            bytes.write((byte) allegedLength);
        } else { // assume shorter
            bytes.write((byte) (CBORConstants.PREFIX_TYPE_BYTES | CBORConstants.SUFFIX_UINT16_ELEMENTS));
            bytes.write((byte) (allegedLength >> 8));
            bytes.write((byte) allegedLength);
        }
        // but only include couple of bytes
        for (int i = 0; i < actualIncluded; ++i) {
            bytes.write((byte) i);
        }
        return bytes.toByteArray();
    }

}
* Reported #185: (cbor) Internal parsing of tagged arrays can lead to stack overflow
* Reported #186: (cbor) Eager allocation of byte buffer can cause `java.lang.OutOfMemoryError`
   exception
 (2.11.4)
2.11.4 (not yet released)

#186: (cbor) Eager allocation of byte buffer can cause `java.lang.OutOfMemoryError`
   exception
 (reported by Paul A)

#219: (avro) Cache record names to avoid hitting class loader