Commit message: Add overflow & overread checks to PngChunk::parseTXTChunk()

This function was creating a lot of new pointers and strings without
properly checking the array bounds. This commit adds several calls
to enforce(), making sure that the pointers stay within bounds.
Strings are now created using the helper function
string_from_unterminated() to prevent overreads in the constructor of
std::string.

This fixes #400
CVE: CVE-2018-16336
CWE: CWE-125
Vulnerability description: Exiv2::Internal::PngChunk::parseTXTChunk in Exiv2 v0.26 allows remote attackers to cause a denial of service (heap-based buffer over-read) via a crafted image file, a different vulnerability than CVE-2018-10999.

Project: exiv2
File: pngchunk_int.cpp

    void PngChunk::decodeIHDRChunk(const DataBuf& data,
                                   int*           outWidth,
                                   int*           outHeight)
    {
        assert(data.size_ >= 8);

        // Extract image width and height from IHDR chunk.

        *outWidth  = getLong((const byte*)data.pData_,     bigEndian);
        *outHeight = getLong((const byte*)data.pData_ + 4, bigEndian);

    } // PngChunk::decodeIHDRChunk

Project: exiv2
File: pngchunk_int.cpp

    void PngChunk::parseChunkContent(      Image*  pImage,
                                     const byte*   key,
                                           long    keySize,
                                     const DataBuf arr)
    {
        // We look if an ImageMagick EXIF raw profile exist.

        if (   keySize >= 21
            && (   memcmp("Raw profile type exif", key, 21) == 0
                || memcmp("Raw profile type APP1", key, 21) == 0)
            && pImage->exifData().empty())
        {
            DataBuf exifData = readRawProfile(arr,false);
            long length      = exifData.size_;

            if (length > 0)
            {
                // Find the position of Exif header in bytes array.

                const byte exifHeader[] = { 0x45, 0x78, 0x69, 0x66, 0x00, 0x00 };
                long pos = -1;

                for (long i=0 ; i < length-(long)sizeof(exifHeader) ; i++)
                {
                    if (memcmp(exifHeader, &exifData.pData_[i], sizeof(exifHeader)) == 0)
                    {
                        pos = i;
                        break;
                    }
                }

                // If found it, store only these data at from this place.

                if (pos !=-1)
                {
#ifdef DEBUG
                    std::cout << "Exiv2::PngChunk::parseChunkContent: Exif header found at position " << pos << "\n";
#endif
                    pos = pos + sizeof(exifHeader);
                    ByteOrder bo = TiffParser::decode(pImage->exifData(),
                                                      pImage->iptcData(),
                                                      pImage->xmpData(),
                                                      exifData.pData_ + pos,
                                                      length - pos);
                    pImage->setByteOrder(bo);
                }
                else
                {
#ifndef SUPPRESS_WARNINGS
                    EXV_WARNING << "Failed to decode Exif metadata.\n";
#endif
                    pImage->exifData().clear();
                }
            }
        }

        // We look if an ImageMagick IPTC raw profile exist.

        if (   keySize >= 21
            && memcmp("Raw profile type iptc", key, 21) == 0
            && pImage->iptcData().empty()) {
            DataBuf psData = readRawProfile(arr,false);
            if (psData.size_ > 0) {
                Blob iptcBlob;
                const byte *record = 0;
                uint32_t sizeIptc = 0;
                uint32_t sizeHdr = 0;

                const byte* pEnd = psData.pData_ + psData.size_;
                const byte* pCur = psData.pData_;
                while (   pCur < pEnd
                       && 0 == Photoshop::locateIptcIrb(pCur,
                                                        static_cast<long>(pEnd - pCur),
                                                        &record,
                                                        &sizeHdr,
                                                        &sizeIptc)) {
                    if (sizeIptc) {
#ifdef DEBUG
                        std::cerr << "Found IPTC IRB, size = " << sizeIptc << "\n";
#endif
                        append(iptcBlob, record + sizeHdr, sizeIptc);
                    }
                    pCur = record + sizeHdr + sizeIptc;
                    pCur += (sizeIptc & 1);
                }
                if (   iptcBlob.size() > 0
                    && IptcParser::decode(pImage->iptcData(),
                                          &iptcBlob[0],
                                          static_cast<uint32_t>(iptcBlob.size()))) {
#ifndef SUPPRESS_WARNINGS
                    EXV_WARNING << "Failed to decode IPTC metadata.\n";
#endif
                    pImage->clearIptcData();
                }
                // If there is no IRB, try to decode the complete chunk data
                if (   iptcBlob.empty()
                    && IptcParser::decode(pImage->iptcData(),
                                          psData.pData_,
                                          psData.size_)) {
#ifndef SUPPRESS_WARNINGS
                    EXV_WARNING << "Failed to decode IPTC metadata.\n";
#endif
                    pImage->clearIptcData();
                }
            } // if (psData.size_ > 0)
        }

        // We look if an ImageMagick XMP raw profile exist.

        if (   keySize >= 20
            && memcmp("Raw profile type xmp", key, 20) == 0
            && pImage->xmpData().empty())
        {
            DataBuf xmpBuf = readRawProfile(arr,false);
            long length    = xmpBuf.size_;

            if (length > 0)
            {
                std::string& xmpPacket = pImage->xmpPacket();
                xmpPacket.assign(reinterpret_cast<char*>(xmpBuf.pData_), length);
                std::string::size_type idx = xmpPacket.find_first_of('<');
                if (idx != std::string::npos && idx > 0)
                {
#ifndef SUPPRESS_WARNINGS
                    EXV_WARNING << "Removing " << idx
                                << " characters from the beginning of the XMP packet\n";
#endif
                    xmpPacket = xmpPacket.substr(idx);
                }
                if (XmpParser::decode(pImage->xmpData(), xmpPacket))
                {
#ifndef SUPPRESS_WARNINGS
                    EXV_WARNING << "Failed to decode XMP metadata.\n";
#endif
                }
            }
        }

        // We look if an Adobe XMP string exist.

        if (   keySize >= 17
            && memcmp("XML:com.adobe.xmp", key, 17) == 0
            && pImage->xmpData().empty())
        {
            if (arr.size_ > 0)
            {
                std::string& xmpPacket = pImage->xmpPacket();
                xmpPacket.assign(reinterpret_cast<char*>(arr.pData_), arr.size_);
                std::string::size_type idx = xmpPacket.find_first_of('<');
                if (idx != std::string::npos && idx > 0)
                {
#ifndef SUPPRESS_WARNINGS
                    EXV_WARNING << "Removing " << idx << " characters "
                                << "from the beginning of the XMP packet\n";
#endif
                    xmpPacket = xmpPacket.substr(idx);
                }
                if (XmpParser::decode(pImage->xmpData(), xmpPacket))
                {
#ifndef SUPPRESS_WARNINGS
                    EXV_WARNING << "Failed to decode XMP metadata.\n";
#endif
                }
            }
        }

        // We look if a comments string exist. Note than we use only 'Description' keyword which
        // is dedicaced to store long comments. 'Comment' keyword is ignored.

        if (   keySize >= 11
            && memcmp("Description", key, 11) == 0
            && pImage->comment().empty())
        {
            pImage->setComment(std::string(reinterpret_cast<char*>(arr.pData_), arr.size_));
        }

    } // PngChunk::parseChunkContent

Project: exiv2
File: pngchunk_int.cpp

    DataBuf PngChunk::parseTXTChunk(const DataBuf& data,
                                    int            keysize,
                                    TxtChunkType   type)
    {
        DataBuf arr;

        if(type == zTXt_Chunk)
        {
            enforce(data.size_ >= Safe::add(keysize, 2), Exiv2::kerCorruptedMetadata);

            // Extract a deflate compressed Latin-1 text chunk

            // we get the compression method after the key
            const byte* compressionMethod = data.pData_ + keysize + 1;
            if ( *compressionMethod != 0x00 )
            {
                // then it isn't zlib compressed and we are sunk
#ifdef DEBUG
                std::cerr << "Exiv2::PngChunk::parseTXTChunk: Non-standard zTXt compression method.\n";
#endif
                throw Error(kerFailedToReadImageData);
            }

            // compressed string after the compression technique spec
            const byte* compressedText      = data.pData_ + keysize + 2;
            unsigned int compressedTextSize = data.size_  - keysize - 2;
            enforce(compressedTextSize < data.size_, kerCorruptedMetadata);

            zlibUncompress(compressedText, compressedTextSize, arr);
        }
        else if(type == tEXt_Chunk)
        {
            enforce(data.size_ >= Safe::add(keysize, 1), Exiv2::kerCorruptedMetadata);
            // Extract a non-compressed Latin-1 text chunk

            // the text comes after the key, but isn't null terminated
            const byte* text = data.pData_ + keysize + 1;
            long textsize    = data.size_  - keysize - 1;

            arr = DataBuf(text, textsize);
        }
        else if(type == iTXt_Chunk)
        {
            enforce(data.size_ >= Safe::add(keysize, 3), Exiv2::kerCorruptedMetadata);
            const int nullSeparators = std::count(&data.pData_[keysize+3], &data.pData_[data.size_], '\0');
            enforce(nullSeparators >= 2, Exiv2::kerCorruptedMetadata);

            // Extract a deflate compressed or uncompressed UTF-8 text chunk

            // we get the compression flag after the key
            const byte compressionFlag   = data.pData_[keysize + 1];
            // we get the compression method after the compression flag
            const byte compressionMethod = data.pData_[keysize + 2];

            enforce(compressionFlag == 0x00 || compressionFlag == 0x01, Exiv2::kerCorruptedMetadata);
            enforce(compressionMethod == 0x00, Exiv2::kerCorruptedMetadata);

            // language description string after the compression technique spec
            const size_t languageTextMaxSize = data.size_ - keysize - 3;
            std::string languageText =
                string_from_unterminated((const char*)(data.pData_ + Safe::add(keysize, 3)), languageTextMaxSize);
            const unsigned int languageTextSize = static_cast<unsigned int>(languageText.size());

            enforce(data.size_ >= Safe::add(static_cast<unsigned int>(Safe::add(keysize, 4)), languageTextSize),
                    Exiv2::kerCorruptedMetadata);
            // translated keyword string after the language description
            std::string translatedKeyText =
                string_from_unterminated((const char*)(data.pData_ + keysize + 3 + languageTextSize + 1),
                                         data.size_ - (keysize + 3 + languageTextSize + 1));
            const unsigned int translatedKeyTextSize = static_cast<unsigned int>(translatedKeyText.size());

            if ((compressionFlag == 0x00) || (compressionFlag == 0x01 && compressionMethod == 0x00)) {
                enforce(Safe::add(static_cast<unsigned int>(keysize + 3 + languageTextSize + 1),
                                  Safe::add(translatedKeyTextSize, 1u)) <= data.size_,
                        Exiv2::kerCorruptedMetadata);

                const byte* text = data.pData_ + keysize + 3 + languageTextSize + 1 + translatedKeyTextSize + 1;
                const long textsize = data.size_ - (keysize + 3 + languageTextSize + 1 + translatedKeyTextSize + 1);

                if (compressionFlag == 0x00) {
                    // then it's an uncompressed iTXt chunk
#ifdef DEBUG
                    std::cout << "Exiv2::PngChunk::parseTXTChunk: We found an uncompressed iTXt field\n";
#endif

                    arr.alloc(textsize);
                    arr = DataBuf(text, textsize);
                } else if (compressionFlag == 0x01 && compressionMethod == 0x00) {
                    // then it's a zlib compressed iTXt chunk
#ifdef DEBUG
                    std::cout << "Exiv2::PngChunk::parseTXTChunk: We found a zlib compressed iTXt field\n";
#endif

                    // the compressed text comes after the translated keyword, but isn't null terminated
                    zlibUncompress(text, textsize, arr);
                }
            } else {
                // then it isn't zlib compressed and we are sunk
#ifdef DEBUG
                std::cerr << "Exiv2::PngChunk::parseTXTChunk: Non-standard iTXt compression method.\n";
#endif
                throw Error(kerFailedToReadImageData);
            }
        }
        else
        {
#ifdef DEBUG
            std::cerr << "Exiv2::PngChunk::parseTXTChunk: We found a field, not expected though\n";
#endif
            throw Error(kerFailedToReadImageData);
        }

        return arr;

    } // PngChunk::parsePngChunk

Project: exiv2
File: pngchunk_int.cpp

    DataBuf PngChunk::readRawProfile(const DataBuf& text,bool iTXt)
    {
        DataBuf                 info;
        register long           i;
        register unsigned char *dp;
        const char             *sp;
        unsigned int            nibbles;
        long                    length;
        unsigned char           unhex[103]={0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,
                                            0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,
                                            0,0,0,0,0,0,0,0,0,1, 2,3,4,5,6,7,8,9,0,0,
                                            0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,
                                            0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,10,11,12,
                                            13,14,15};
        if (text.size_ == 0) {
            return DataBuf();
        }

        if ( iTXt ) {
            info.alloc(text.size_);
            ::memcpy(info.pData_,text.pData_,text.size_);
            return  info;
        }


        sp = (char*)text.pData_+1;

        // Look for newline

        while (*sp != '\n')
            sp++;

        // Look for length

        while (*sp == '\0' || *sp == ' ' || *sp == '\n')
            sp++;

        length = (long) atol(sp);

        while (*sp != ' ' && *sp != '\n')
            sp++;

        // Allocate space

        if (length == 0)
        {
#ifdef DEBUG
            std::cerr << "Exiv2::PngChunk::readRawProfile: Unable To Copy Raw Profile: invalid profile length\n";
#endif
        }

        info.alloc(length);

        if (info.size_ != length)
        {
#ifdef DEBUG
            std::cerr << "Exiv2::PngChunk::readRawProfile: Unable To Copy Raw Profile: cannot allocate memory\n";
#endif
            return DataBuf();
        }

        // Copy profile, skipping white space and column 1 "=" signs

        dp      = (unsigned char*)info.pData_;
        nibbles = length * 2;

        for (i = 0; i < (long) nibbles; i++)
        {
            while (*sp < '0' || (*sp > '9' && *sp < 'a') || *sp > 'f')
            {
                if (*sp == '\0')
                {
#ifdef DEBUG
                    std::cerr << "Exiv2::PngChunk::readRawProfile: Unable To Copy Raw Profile: ran out of data\n";
#endif
                    return DataBuf();
                }

                sp++;
            }

            if (i%2 == 0)
                *dp = (unsigned char) (16*unhex[(int) *sp++]);
            else
                (*dp++) += unhex[(int) *sp++];
        }

        return info;

    } // PngChunk::readRawProfile

Project: exiv2
File: pngchunk_int.cpp

    DataBuf PngChunk::keyTXTChunk(const DataBuf& data, bool stripHeader)
    {
        // From a tEXt, zTXt, or iTXt chunk,
        // we get the key, it's a null terminated string at the chunk start
        const int offset = stripHeader ? 8 : 0;
        if (data.size_ <= offset) throw Error(kerFailedToReadImageData);
        const byte *key = data.pData_ + offset;

        // Find null string at end of key.
        int keysize=0;
        while (key[keysize] != 0)
        {
            keysize++;
            // look if keysize is valid.
            if (keysize+offset >= data.size_)
                throw Error(kerFailedToReadImageData);
        }

        return DataBuf(key, keysize);

    } // PngChunk::keyTXTChunk