Orthanc DICOM Server
Out-of-Bounds Read in DicomStreamReader Meta-Header Parser
Orthanc's DICOM stream parser allocates a meta-header block sized by the group-length tag, then reads each tag's declared value length and assigns that many bytes from the block. The loop guard verifies the 8/12-byte tag header fits, but not the value length — so a truncated group-length value lets the parser read past the allocation boundary.
Description
Orthanc's POST /instances handler streams uploaded DICOM data through DicomStreamReader::HandleMetaHeader. The parser allocates a heap block whose size comes from the group-length tag (0002,0000), then iterates over the meta-header tags inside that block. Each iteration verifies that the tag's 8-byte (short-form) or 12-byte (long-form) header fits within the allocation before reading the tag's value length.
The short-form path reads a 16-bit value length and copies that many bytes out of the block — without first checking that pos + 8 + length stays within block.size():
OrthancFramework/Sources/DicomFormat/DicomStreamReader.cpp:197-228
while (pos + 8 <= block.size()) // checks 8 bytes available{ DicomTag tag = ReadTag(p + pos, true); ValueRepresentation vr = StringToValueRepresentation( std::string(p + pos + 4, 2), true);
if (IsShortExplicitTag(vr)) { uint16_t length = ReadUnsignedInteger16(p + pos + 6, true); // untrusted length
std::string value; value.assign(p + pos + 8, length); // NO CHECK: pos+8+length <= block.size() pos += length + 8; }The long-form path uses a 32-bit value length and has the same gap:
OrthancFramework/Sources/DicomFormat/DicomStreamReader.cpp:230-248
else if (pos + 12 <= block.size()) { uint32_t length = ReadUnsignedInteger32(p + pos + 8, true); // untrusted 32-bit length
if (tag.GetGroup() == 0x0002) { std::string value; value.assign(p + pos + 12, length); // NO CHECK: pos+12+length <= block.size() }
pos += length + 12; }The same parsing logic in DicomMap::ReadNextTag includes the corresponding bounds check:
OrthancFramework/Sources/DicomFormat/DicomMap.cpp:1096
uint16_t length = ReadLittleEndianUint16(dicom + position + 6);if (position + 8 + length > size) // THIS CHECK IS MISSING IN DicomStreamReader{ return false;}value.assign(dicom + position + 8, length);If the group-length tag (0002,0000) is patched to a value small enough that a target tag's 8-byte header still fits inside the block but its value bytes extend past the allocation, value.assign() then reads beyond the heap allocation. AddressSanitizer pinpoints the OOB at DicomStreamReader.cpp:208, immediately past a 157-byte allocation made by StreamBlockReader::Schedule:
AddressSanitizer trace
==1==ERROR: AddressSanitizer: heap-buffer-overflow on address 0xffff9423cebdREAD of size 64 at 0xffff9423cebd thread T16 #0 in __interceptor_memcpy #3 in std::__cxx11::basic_string::assign() basic_string.h:1443 #4 in Orthanc::DicomStreamReader::HandleMetaHeader() DicomStreamReader.cpp:208 #8 in Orthanc::DicomStreamReader::LookupPixelDataOffset() DicomStreamReader.cpp:740 #9 in Orthanc::ServerContext::StoreAfterTranscoding() ServerContext.cpp:683 #13 in UploadDicomFile OrthancRestApi.cpp:246
0xffff9423cebd is located 0 bytes to the right of 157-byte region [0xffff9423ce20,0xffff9423cebd)allocated by thread T16 here: #4 in Orthanc::StreamBlockReader::Schedule() DicomStreamReader.cpp:49Two structural factors limit the practical impact. First, DCMTK pre-validates the DICOM file before DicomStreamReader runs; tag value lengths inflated to large values (e.g. 0xFFFF) get the file rejected up-front, capping the OOB read to approximately the original tag value length (~64 bytes). Second, the value bytes are written into a temporary std::string that VisitMetaHeaderTag() inspects only for the Transfer Syntax UID — for any other tag the string is destroyed on stack unwind, with no exfiltration channel in the studied path.
The C-STORE network protocol does not transmit file meta-headers (Orthanc generates its own correct meta-header for C-STORE-received instances), so this vector is HTTP-only.
Impact
- Out-of-bounds heap read of approximately 64 bytes during DICOM file upload via
POST /instances. - No exfiltration channel exists in the studied path: the affected tag's value is consumed by
VisitMetaHeaderTag(), which only checks for Transfer Syntax UID, then discarded. Information disclosure is theoretical. - The 64-byte read stays within accessible heap pages on stock builds; no crash or denial of service is observed.
Mitigation
Update Orthanc to version 1.12.11 or later. The fix adds the missing pos + 8 + length <= block.size() and pos + 12 + length <= block.size() checks before the value.assign() calls, matching the existing logic in DicomMap::ReadNextTag.
Defender's Checklist
Verify your version.
curl -u <user>:<pass> http://<orthanc>:8042/system | jq .Version— the patched range begins at 1.12.11.Restrict the affected endpoint.
Restrict
POST /instancesat your reverse proxy to the IP ranges of the imaging modalities and integration services that legitimately upload studies.Audit credentials.
Even though the original CVSS uses PR:N, deployments with HTTP authentication on
/instancesrequire an authenticated user. ReviewRegisteredUsersinorthanc.jsonand any external authentication backend for stale or over-permissioned accounts.Check upload logs for malformed DICOM.
Look for
POST /instancescalls that produced anomalous responses and for parser-level error entries referencing meta-header parsing.Plan an ASAN-instrumented test build.
For high-risk environments, validate the patched build against your modality-vendor sample DICOM with an AddressSanitizer-instrumented Orthanc to confirm no regression and no remaining OOB reads in adjacent meta-header paths.
Severity Reasoning
POST /instances.AC:LA single crafted DICOM with a truncated meta-header. No timing or environmental conditions.PR:NThe published vector reflects deployments where POST /instances is exposed without HTTP authentication, which Orthanc permits via configuration.UI:NNo interaction by a second user is needed.S:UOnly the Orthanc process is affected; no out-of-process resources are reachable through this primitive.C:NIn the studied call path the OOB-read bytes are consumed only for the Transfer Syntax UID check and then discarded; no exfiltration channel exists.I:NRead-only primitive into a discarded buffer; nothing is modified.A:HOn builds with strict heap layout the OOB read can reach an unmapped memory page, terminating the Orthanc process.On stock 1.12.10 builds with default glibc, the 64-byte read typically stays within accessible heap pages and does not crash; the published A:H reflects the worst case on hardened or restricted-heap deployments.
References
How We Can Help
Who We Are
The security researchers behind this advisory.

Dr. rer. nat. Simon Weber
Senior Pentester & MedSec Researcher
I evaluate your SaMD with the same industry-defining security insight I contributed to the BAK MV for the revision of the B3S standard.
- PhD on Hospital Cybersecurity
- Critical vulnerabilities found in hospital systems
- Alumni of THB MedSec Research Group
- gematik Security Hero

Dipl.-Inf. Volker Schönefeld
Senior Application Security Expert
As a former CTO and developer turned pentester, I work alongside your team to uncover vulnerabilities and find solutions that fit your architecture.
- 20+ years as CTO, 50M+ app downloads
- Architected and secured large-scale IoT fleets
- Certified Web Exploitation Specialist
- gematik Security Hero
Looking for a Penetration Test?
Machine Spirits specializes in security assessments for medical devices and healthcare IT. From MDR penetration testing to C5 cloud compliance, we help MedTech companies meet regulatory requirements.
