Bug 259 - Security report: reachable heap-buffer-overflow in `/tools/create-dicom` PNG handling
Summary: Security report: reachable heap-buffer-overflow in `/tools/create-dicom` PNG ...
Status: RESOLVED FIXED
Alias: None
Product: Orthanc
Classification: Unclassified
Component: Orthanc Core (show other bugs)
Version: unspecified
Hardware: PC Linux
: --- critical
Assignee: Sébastien Jodogne
URL:
Depends on:
Blocks:
 
Reported: 2026-05-08 11:35 CEST by Michael Knap
Modified: 2026-06-01 21:28 CEST (History)
2 users (show)

See Also:


Attachments
PoC (2.38 KB, text/x-python)
2026-05-08 11:35 CEST, Michael Knap
Details

Note You need to log in before you can comment on or make changes to this bug.
Description Michael Knap 2026-05-08 11:35:23 CEST
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.
Comment 1 Alain Mazy 2026-06-01 21:28:03 CEST
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.