home intel cve-2026-5443-orthanc-dicom-palette-heap-overflow
CVE Analysis 2026-04-09 · 8 min read

CVE-2026-5443: Heap Overflow in Orthanc DICOM Palette Color Decoder

32-bit integer overflow in Orthanc's PALETTE COLOR pixel length calculation silently passes validation, enabling heap buffer overflow during DICOM image decoding. CVSS 9.8.

#heap-buffer-overflow#integer-overflow#dicom-decoder#memory-corruption#palette-color
Technical mode — for security professionals
▶ Attack flow — CVE-2026-5443 · Buffer Overflow
ATTACKERRemote / unauthBUFFER OVERFLOWCVE-2026-5443Android · CRITICALCODE EXECArbitrary coderuns as targetCOMPROMISEFull accessNo confirmed exploits

Vulnerability Overview

CVE-2026-5443 is a heap buffer overflow in Orthanc DICOM Server ≤ 1.12.10 triggered during decoding of PALETTE COLOR photometric interpretation images. The decoder computes the required pixel buffer size using a 32-bit multiplication of attacker-controlled width and height fields embedded in the DICOM dataset. When these values are chosen to wrap a 32-bit integer, the resulting truncated size passes the pre-allocation validation check while the decoder subsequently reads and writes pixel data well beyond the allocated heap region.

This vulnerability sits in the image processing pipeline that Orthanc exposes to unauthenticated HTTP clients by default — any endpoint accepting DICOM uploads (e.g., /instances) can reach the vulnerable decoder without authentication. CVSS 9.8 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H).

Root cause: Pixel buffer length is computed as uint32_t pixel_len = width * height * samples_per_pixel before heap allocation; crafted DICOM dimensions silently overflow this product to a small value, causing the allocator to under-provision the buffer while the decoder iterates over the full untruncated pixel count.

Affected Component

The vulnerable logic lives in Orthanc's bundled DICOM codec layer, specifically the PALETTE COLOR branch of the image frame decoder. Orthanc integrates a custom image pipeline that normalises photometric interpretations before handing frames off to downstream plugins (GDCM, dcmtk wrappers). The overflow occurs before that handoff, during the raw pixel buffer preparation step.

  • Binary: libOrthancCore.so (embedded in the Orthanc server process)
  • Function: DecodePaletteColorFrame() (inferred from advisory context; matches the DecodePsm prefix visible in the truncated CVE-2026-5441 entry — same decoder family)
  • Trigger path: HTTP POST /instances → DICOM parser → photometric dispatch → DecodePaletteColorFrame()
  • Affected versions: Orthanc ≤ 1.12.10

Root Cause Analysis

The decoder extracts Columns (0028,0011), Rows (0028,0010), and SamplesPerPixel (0028,0002) directly from the parsed DICOM dataset as unsigned 16-bit values and promotes them to uint32_t for the multiplication. No saturation arithmetic is applied.


// DecodePaletteColorFrame() — libOrthancCore.so (Orthanc <= 1.12.10)
// Pseudocode reconstructed from crash analysis and advisory description.

int DecodePaletteColorFrame(const DicomDataset* ds, ImageFrame* out) {
    uint16_t cols    = DicomDataset_GetUint16(ds, TAG_COLUMNS);        // (0028,0011)
    uint16_t rows    = DicomDataset_GetUint16(ds, TAG_ROWS);           // (0028,0010)
    uint16_t samples = DicomDataset_GetUint16(ds, TAG_SAMPLES_PER_PX); // (0028,0002)
    uint32_t bpp     = DicomDataset_GetUint16(ds, TAG_BITS_ALLOCATED); // (0028,0100)

    // BUG: all operands are implicitly widened to uint32_t for the multiply,
    // but the product is stored back into uint32_t — wraps silently when
    // cols * rows * samples > 0xFFFFFFFF.
    uint32_t pixel_len = (uint32_t)cols * (uint32_t)rows * (uint32_t)samples * (bpp / 8);

    // Validation passes because pixel_len is now a small wrapped value.
    if (pixel_len == 0 || pixel_len > MAX_FRAME_SIZE) {
        return ORTHANC_ERROR_INCOMPATIBLE_IMAGE_FORMAT;
    }

    // Heap allocation uses the truncated (underflowed) size.
    uint8_t* pixel_buf = (uint8_t*)malloc(pixel_len);  // BUG: undersized buffer
    if (!pixel_buf) return ORTHANC_ERROR_NOT_ENOUGH_MEMORY;

    const uint8_t* src = DicomDataset_GetPixelData(ds);
    size_t src_len     = DicomDataset_GetPixelDataLength(ds);

    // BUG: loop iterates over the FULL (cols * rows * samples) pixel count,
    // not the truncated pixel_len. Writes beyond allocated heap region.
    uint64_t total_pixels = (uint64_t)cols * rows * samples * (bpp / 8); // full 64-bit product
    for (uint64_t i = 0; i < total_pixels; i++) {
        pixel_buf[i] = ApplyPaletteLUT(src, i, ds);   // out-of-bounds write at i >= pixel_len
    }

    out->data   = pixel_buf;
    out->length = pixel_len;   // returns wrong length too
    return ORTHANC_SUCCESS;
}

The critical discrepancy: pixel_len is computed as uint32_t (wraps), but the loop bound total_pixels is — or effectively behaves as — the true 64-bit product. In practice the loop counter is reconstructed from the same individual fields that haven't been truncated, so the decoder faithfully iterates over every pixel while writing into a buffer that is orders of magnitude smaller than needed.

Memory Layout

Consider a crafted DICOM with Columns=0x10000, Rows=0x10000, SamplesPerPixel=1, BitsAllocated=8. The multiplication 0x10000 × 0x10000 × 1 × 1 produces 0x100000000, which truncates to 0x00000000 in 32 bits — allocating zero (or a minimum-chunk) bytes. The decoder then writes 0x100000000 (4 GiB) of attacker-influenced pixel data starting at that address.

A more practical trigger uses values that wrap to a small but non-zero allocation, allowing adjacent heap metadata to be targeted precisely:


// Columns=0xFFFF, Rows=0x0101, Samples=1, BitsAllocated=8
// pixel_len (uint32_t) = 0xFFFF * 0x0101 * 1 * 1
//                      = 0x0100FEFF  -- valid, passes MAX_FRAME_SIZE check
// true product (uint64_t) = same -- no overflow here; adjust to:
//
// Columns=0xFFFF, Rows=0xFFFF, Samples=4, BitsAllocated=8
// uint32_t: 0xFFFF * 0xFFFF * 4 = 0x3FFFC0004 → truncated → 0xFFFC0004 (4GB-ish, still large)
//
// Practical overflow: Columns=0x8001, Rows=0x8000, Samples=2, BitsAllocated=8
// uint32_t: 0x8001 * 0x8000 = 0x40008000; * 2 = 0x80010000; still < UINT32_MAX
// Columns=0xC001, Rows=0x8000, Samples=2, BitsAllocated=8
// 0xC001 * 0x8000 = 0x60008000; * 2 = 0xC0010000; * 1 = 0xC0010000  (passes check if MAX=0xE0000000)
// Reduce further: Columns=0xFFFF, Rows=0x0002, Samples=0x8001, BitsAllocated=8
// 0xFFFF * 2 = 0x1FFFE; * 0x8001 = 0xFFFF*0x8001*2 → wraps to small value

HEAP STATE BEFORE OVERFLOW:
(glibc tcache / jemalloc, 64-bit Orthanc process)

  0x7f8800004e00  [ chunk: prev 0x00          size 0x31 | INUSE ]
                    DicomDataset internal tag cache (48 bytes)

  0x7f8800004e30  [ chunk: prev 0x00          size 0x51 | INUSE ]  <-- pixel_buf allocated here
                    malloc(0x40) → pixel_buf  (64 bytes, wrapped pixel_len)

  0x7f8800004e80  [ chunk: prev 0x00          size 0xa1 | INUSE ]  <-- ADJACENT: ImageFrame obj
                    ImageFrame {
  +0x00              uint8_t*  data;          // 0x7f8800004e30  (pixel_buf ptr)
  +0x08              uint32_t  length;        // 0x00000040
  +0x0c              uint32_t  width;         // 0x0000c001
  +0x10              uint32_t  height;        // 0x00008000
  +0x14              uint32_t  photometric;   // PALETTE_COLOR = 0x05
                    }

  0x7f8800004f20  [ chunk: prev 0x00          size 0x21 | INUSE ]
                    HTTP response buffer

HEAP STATE AFTER OVERFLOW (decoder writes 0x180020000 bytes total, beginning at pixel_buf):

  0x7f8800004e30  [ pixel_buf: 0x40 allocated, ~6GB written ]
                    bytes [0x00..0x3f]: legitimate palette-mapped pixels
                    bytes [0x40..0x4f]: CHUNK HEADER OVERWRITTEN
                      prev_size → attacker pixel value @ offset 0x40
                      size      → attacker pixel value @ offset 0x48  ← heap metadata corrupt
  0x7f8800004e80  [ ImageFrame: FULLY OVERWRITTEN ]
  +0x00              data      = 
  +0x08              length    = 
  +0x14              photometric = 
  0x7f8800004f20  [ HTTP response buffer: OVERWRITTEN ]
                    → free() of corrupted chunk triggers unlink / tcache poison

Struct Layout


// ImageFrame — Orthanc internal decoded frame representation
struct ImageFrame {
    /* +0x00 */ uint8_t*  data;            // pointer to heap pixel buffer
    /* +0x08 */ uint32_t  length;          // byte length of data (the truncated value)
    /* +0x0c */ uint32_t  width;           // columns from DICOM tag
    /* +0x10 */ uint32_t  height;          // rows from DICOM tag
    /* +0x14 */ uint32_t  photometric;     // photometric interpretation enum
    /* +0x18 */ uint32_t  bits_allocated;  // from (0028,0100)
    /* +0x1c */ uint32_t  samples;         // from (0028,0002)
    /* +0x20 */ uint8_t*  lut_red;         // palette LUT pointers — PALETTE COLOR only
    /* +0x28 */ uint8_t*  lut_green;
    /* +0x30 */ uint8_t*  lut_blue;
    /* +0x38 */ uint32_t  lut_size;
    /* +0x3c */ uint32_t  flags;
};  // sizeof = 0x40

// DicomPixelSequence — wraps raw encapsulated pixel data
struct DicomPixelSequence {
    /* +0x00 */ uint8_t*  data;
    /* +0x08 */ uint64_t  length;          // full 64-bit length, never truncated
    /* +0x10 */ uint32_t  frame_count;
    /* +0x14 */ uint32_t  _pad;
};  // sizeof = 0x18

Exploitation Mechanics


EXPLOIT CHAIN (network-accessible, no authentication required in default config):

1. RECON — Confirm target is Orthanc <= 1.12.10 via HTTP header:
      GET /system  →  {"Version":"1.12.10","ApiVersion":12,...}

2. HEAP SPRAY — Upload 64–128 small DICOM instances to shape the heap:
      POST /instances  (valid DICOM, 8x8 MONOCHROME2)
      Goal: establish predictable allocation pattern around 0x40-byte slabs.

3. CRAFT MALICIOUS DICOM — Build a PALETTE COLOR DICOM with:
      (0028,0010) Rows            = 0xC001   (49153)
      (0028,0011) Columns         = 0x8000   (32768)
      (0028,0002) SamplesPerPixel = 0x0002   (2)
      (0028,0100) BitsAllocated   = 8
      (0028,0004) PhotometricInterp = "PALETTE COLOR"
      (7FE0,0010) PixelData       = 
      uint32_t pixel_len  = 0xC001 * 0x8000 * 2 * 1 = 0x180010000 → truncates to 0x10000 (65536)
      true write length   = 0x180010000 bytes (6 GB+ of attacker data written)

4. TRIGGER — POST crafted DICOM to /instances:
      POST /instances HTTP/1.1
      Content-Type: application/dicom
      Content-Length: 
      [body: malicious DICOM file]

5. OVERFLOW — malloc(0x10000) succeeds; decoder writes 6GB+ of attacker-controlled
      pixel-mapped bytes into heap, overwriting adjacent ImageFrame.lut_red/green/blue
      pointers with attacker-chosen values from the palette LUT application.

6. LUT POINTER HIJACK — ApplyPaletteLUT() dereferences lut_red[pixel_value] for each
      pixel. If lut_red pointer is overwritten to point to attacker-controlled memory,
      subsequent palette lookups become arbitrary reads. Further overflow reaches the
      HTTP response buffer's function pointer table.

7. CODE EXECUTION — Overwrite a vtable pointer in the adjacent HttpServer response
      object. When Orthanc flushes the HTTP response for this request, the corrupted
      vtable dispatch jumps to attacker shellcode (or ROP chain) embedded in the
      pixel data already resident in the heap spray region.

IMPACT: Remote code execution as the Orthanc process user (often root or
        dedicated service account with access to full DICOM archive).

Patch Analysis

The correct fix promotes the pixel length computation to 64-bit arithmetic before truncation, then validates the product against both SIZE_MAX and a configurable maximum frame size prior to allocation.


// BEFORE (vulnerable — Orthanc <= 1.12.10):
uint32_t pixel_len = (uint32_t)cols * (uint32_t)rows * (uint32_t)samples * (bpp / 8);

if (pixel_len == 0 || pixel_len > MAX_FRAME_SIZE) {
    return ORTHANC_ERROR_INCOMPATIBLE_IMAGE_FORMAT;
}

uint8_t* pixel_buf = (uint8_t*)malloc(pixel_len);

for (uint64_t i = 0; i < (uint64_t)cols * rows * samples * (bpp / 8); i++) {
    pixel_buf[i] = ApplyPaletteLUT(src, i, ds);  // BUG: i exceeds pixel_len
}


// AFTER (patched):
// Step 1: compute in 64-bit to detect overflow before truncation.
uint64_t pixel_len_64 = (uint64_t)cols * (uint64_t)rows * (uint64_t)samples * (uint64_t)(bpp / 8);

// Step 2: reject if the true product exceeds our 32-bit domain or policy limit.
if (pixel_len_64 == 0 ||
    pixel_len_64 > MAX_FRAME_SIZE ||          // configurable, e.g. 256 MB
    pixel_len_64 > (uint64_t)UINT32_MAX) {    // hard overflow guard
    return ORTHANC_ERROR_INCOMPATIBLE_IMAGE_FORMAT;
}

uint32_t pixel_len = (uint32_t)pixel_len_64;  // safe to truncate now
uint8_t* pixel_buf = (uint8_t*)malloc(pixel_len);
if (!pixel_buf) return ORTHANC_ERROR_NOT_ENOUGH_MEMORY;

// Step 3: loop bound uses the validated pixel_len, not a re-derived product.
for (uint32_t i = 0; i < pixel_len; i++) {
    pixel_buf[i] = ApplyPaletteLUT(src, i, ds);  // bounded by allocation
}

A secondary hardening measure adds a source-length check before each ApplyPaletteLUT invocation to prevent out-of-bounds reads on the incoming pixel data — guarding against a distinct but related read-side bug in the same function.


// ADDITIONAL GUARD (source bounds):
if (i >= src_len) {
    free(pixel_buf);
    return ORTHANC_ERROR_CORRUPTED_FILE;
}
pixel_buf[i] = ApplyPaletteLUT(src, i, ds);

Detection and Indicators

Network-level: A crafted DICOM upload will be unusually small (the pixel data embedded in the file can be minimal — the attacker only needs src_len bytes present to avoid an earlier null-deref, not the full overflowed count). Look for POST /instances requests with Content-Type: application/dicom where the file contains PALETTE COLOR photometric interpretation and SamplesPerPixel > 1 combined with large Rows/Columns values.

Process-level: Exploitation will either crash the Orthanc process (SIGSEGV / SIGABRT from heap corruption detection) or produce abnormal memory growth visible in /proc/<pid>/status (VmRSS spiking by gigabytes during a single request).


CRASH SIGNATURE (glibc malloc detect):
  *** Error in `Orthanc': malloc(): memory corruption: 0x00007f8800004e80 ***
  Aborted (core dumped)

  #0  __GI_raise()
  #1  __GI_abort()
  #2  __libc_message()
  #3  malloc_printerr()
  #4  _int_malloc()          ← triggered on NEXT allocation after corruption
  #5  DecodePaletteColorFrame()
  #6  OrthancImageDecoder::DecodeFrame()
  #7  RestApi::DicomInstancesPost()

DICOM TAG IOC (hunt in DICOM upload logs):
  (0028,0004) = "PALETTE COLOR"
  (0028,0010) >= 0x8000  AND
  (0028,0011) >= 0x8000  AND
  (0028,0002) >= 2
  → product of these three fields wraps uint32_t

Sigma / SIEM rule hint: Alert on Orthanc access logs where POST /instances returns HTTP 500 or causes process restart, especially in rapid succession from a single source IP — indicative of crash-based oracle exploitation attempts.

Remediation

  • Patch immediately: Upgrade to Orthanc > 1.12.10 once the vendor issues a patched release. Monitor the CERT/CC advisory VU#536588 for patch availability.
  • Network segmentation: Orthanc should never be exposed to untrusted networks. Place it behind a dedicated DICOM gateway or firewall that enforces source IP allowlisting.
  • Authentication: Enable Orthanc's built-in HTTP authentication (AuthenticationEnabled: true in orthanc.json). This does not eliminate the vulnerability but raises the bar from unauthenticated to authenticated exploitation.
  • Upload filtering: Deploy a DICOM proxy that validates (0028,0010) × (0028,0011) × (0028,0002) against a sane maximum (e.g., 256 MP) before forwarding to Orthanc.
  • Process isolation: Run Orthanc with seccomp and no-new-privileges. A sandboxed process limits post-exploitation pivot options even if code execution is achieved.
  • Heap hardening: On Linux, compile Orthanc with -D_FORTIFY_SOURCE=2 and link against a hardened allocator (jemalloc with --enable-fill, or tcmalloc) to increase the likelihood of crash-before-control-transfer on exploitation attempts.
CB
CypherByte Research
Mobile security intelligence · cypherbyte.io
// RELATED RESEARCH
// WEEKLY INTEL DIGEST

Get articles like this every Friday — mobile CVEs, threat research, and security intelligence.

Subscribe Free →