Created attachment 151 [details] PoC Hello Orthanc maintainers, I found a reachable memory-corruption issue in the Orthanc REST endpoint `POST /tools/create-dicom`. A request that embeds a crafted PNG image in the JSON `Content` data URI field can make Orthanc allocate an undersized heap buffer and then pass out-of-bounds row pointers to libpng. The image is supplied directly in the REST request body: ```json { "Tags": { "PatientName": "Crash^PNG", "PatientID": "PNG-OOB-POC" }, "Content": "data:image/png;base64,..." } ``` ## Classification - Suggested severity: High - Suggested CWE: CWE-190 leading to CWE-787 / CWE-122 - Suggested CVSS 3.1: `7.6 High` - Suggested vector: `CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:H` The `PR:L` score assumes the REST API requires authentication or another access-control layer. If the REST API is exposed without authentication, the privilege requirement becomes `PR:N`, which gives `CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:H` / `8.6 High`. ## Affected Path Endpoint: ```text POST /tools/create-dicom ``` Code path: ```text OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp CreateDicomV2() -> ParsedDicomFile::EmbedContent() OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp EmbedContentInternal() -> EmbedImage(mime, content) OrthancFramework/Sources/Images/PngReader.cpp PngReader::Read() -> png_read_image() ``` Relevant source references from the reviewed tree: - `PngReader.cpp:143-150`: reads PNG width and height - `PngReader.cpp:152-183`: computes `unsigned int pitch` - `PngReader.cpp:195`: allocates `data_.resize(height * pitch)` - `PngReader.cpp:206-212`: builds row pointers and calls `png_read_image()` - `ParsedDicomFile.cpp:1171-1186`: dispatches PNG data URI content - `OrthancRestAnonymizeModify.cpp:1033`: `CreateDicomV2()` embeds request `Content` There is a related arithmetic pattern later in `ParsedDicomFile::EmbedImage()`: - `ParsedDicomFile.cpp:1425-1427`: computes `accessor.GetHeight() * pitch` in 32-bit arithmetic before allocating DICOM pixel data. ## Root Cause `PngReader::Read()` stores row pitch and total decoded image size in 32-bit `unsigned int` arithmetic. For an RGB PNG, the code computes: ```text pitch = width * 3 data_.resize(height * pitch) ``` The attached reproducer uses: ```text width = 65536 height = 21846 pitch = 65536 * 3 = 196608 real decoded size = 21846 * 196608 = 4295098368 32-bit wrapped allocation size = 131072 first decoded row write = 196608 ``` The allocation is therefore only 128 KiB, while the first decoded row alone is 192 KiB. The PNG uses filter type 0 and RGB bytes of `0x41`, so the overflow bytes are deterministic decoded image data. ## Reproduction I attached a self-contained Python reproducer: ```text orthanc-create-dicom-png-oob-poc.py ``` Run Orthanc normally, then send the request: ```sh /tmp/orthanc-build-nosan/Orthanc /tmp/orthanc-png-oob-nosan-config.json --verbose python3 orthanc-create-dicom-png-oob-poc.py --host 127.0.0.1 --port 8052 ``` The reproducer generates a complete PNG in memory and sends it as a data URI in the JSON body. The generated PNG is about 4.2 MB and the JSON request is about 5.6 MB. I did not override `MaximumRequestBodySizeMB`; Orthanc used the default request body limit in my build: ```text Limiting the maximum body size in HTTP requests to 8192MB ``` Observed result in a non-sanitized `RelWithDebInfo` build: ```text POST /tools/create-dicom Segmentation fault (core dumped) exit status 139 ``` Client-side result: ```text http.client.RemoteDisconnected: Remote end closed connection without response ``` A GDB run of the same input reports `SIGSEGV` while libpng is copying rows: ```text Thread "civetweb-worker" received signal SIGSEGV png_read_row() png_read_image() Orthanc::PngReader::Read(...) / OrthancFramework/Sources/Images/PngReader.cpp:212 Orthanc::PngReader::ReadFromMemory(...) / OrthancFramework/Sources/Images/PngReader.cpp:307 Orthanc::ParsedDicomFile::EmbedImage(...) / OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp:1288 Orthanc::ParsedDicomFile::EmbedContentInternal(...) / OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp:1186 Orthanc::CreateDicomV2(...) / OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp:1033 ``` AddressSanitizer confirms the root cause: ```text ERROR: AddressSanitizer: heap-buffer-overflow WRITE of size 196608 in png_combine_row / png_read_row / png_read_image called from Orthanc::PngReader::Read(...) ``` ASAN reports the undersized allocation at `PngReader.cpp:195` and the overflowing decode at `PngReader.cpp:212`. ## Impact The confirmed impact is a remotely triggerable crash of the Orthanc process for clients that can access the REST endpoint. ASAN confirms that the crash is caused by a heap out-of-bounds write of attacker-controlled decoded PNG bytes. I did not attempt heap shaping or control-flow exploitation. The potential impact beyond denial of service depends on allocator layout and adjacent heap objects in a target build. ## Workaround and Fix Lowering `MaximumRequestBodySizeMB` below the attached reproducer's approximately 5.6 MB request size blocks this exact PoC. This is not the default in my build and should be treated only as defense in depth, not as a complete fix. The underlying issue is unchecked decoded-image dimension arithmetic. Recommended fixes: - Use checked `uint64_t` or `size_t` arithmetic for `bytes_per_pixel * width`. - Use checked `uint64_t` or `size_t` arithmetic for `height * pitch`. - Reject images whose decoded size exceeds a sane configured pixel or memory limit. - Verify libpng rowbytes against the checked expected pitch before decoding. - Consider setting libpng user limits before decode. - Apply the same checked-allocation pattern to `ParsedDicomFile::EmbedImage()` and review JPEG/image paths that compute `pitch * height` with `unsigned int`. ## Test Environment Source revision: ```text Mercurial revision: 9551353f3e037e791ed09f34adbd483c0076a0e0 Short revision: 9551353f3e03 Commit date: 2026-05-06 16:55 +0200 ``` Build/test environment: ```text Linux x86_64 Clang 22.1.3 CMake 4.3.2 Ninja 1.13.2 DCMTK 3.7.0 libpng 1.6.58 OpenSSL 3.6.2 ``` Thank you for maintaining Orthanc. I am happy to provide the ASAN log, test configuration, or a proposed patch if useful.
Hi @Michael, Thanks for reporting the issue. Something very similar had already been reported last month by someone else and therefore, your PoC does not trigger anymore on the mainline code. Fixed in this commit: https://github.com/orthanc-mirrors/orthanc/commit/aedb4d00d26844cc0e3162d1ca6a38c8806ea902 Here is the output of your PoC as run today: python orthanc-create-dicom-png-oob-poc.py PNG bytes: 4216389 JSON bytes: 5621955 Expected Orthanc allocation: 131072 bytes First decoded row write: 196608 bytes HTTP status: 400 Bad Request { "Details" : "PNG IMAGE size overflow (4295098368 vs 4294967296)", "HttpError" : "Bad Request", "HttpStatus" : 400, "Message" : "Bad file format", "Method" : "POST", "OrthancError" : "Bad file format", "OrthancStatus" : 15, "Uri" : "/tools/create-dicom" } Thanks and best regards, Alain.