Bug #1198 » IC-DCMTK-0024_REPORT.md
IC-DCMTK-0024: Path Traversal in JSON Bulkdata Loading
Version: DCMTK master 418274445 (DCMTK-3.7.0+64)
CWE: CWE-22 (Path Traversal)
Description
A TOCTOU (time-of-check-to-time-of-use) race condition exists in DcmJSONReader::loadBulkdataFile() in dcmdata/libsrc/dcjsonrd.cc, lines 694-710. The JSON DICOM reader processes BulkDataURI values referencing local files via file:// URIs. The code implements path canonicalization via realpath() and checks the resolved path against a list of permitted directories, but between the realpath() resolution and the subsequent fopen() call, an attacker with write access to the permitted directory could swap a symlink to redirect the read to an arbitrary location.
Exploitation requires the attacker to have write access to a permitted bulkdata directory. Static symlink-based traversal (e.g., ../../etc/passwd) is correctly defeated by realpath(). However, we have confirmed through a multi-threaded PoC that the TOCTOU race can be won in practice: in testing, 3 out of 3,000 attempts successfully redirected the file read to /etc/hostname, embedding the target file's content into the DICOM output.
// dcjsonrd.cc:694 -- TIME OF CHECK
result = normalizePath(filePath, filePathNormalized); // realpath() resolves symlinks
// dcjsonrd.cc:702 -- PATH VALIDATION
if (! bulkdataPathPermitted(filePathNormalized)) { ... }
// dcjsonrd.cc:710 -- TIME OF USE (symlink could have been swapped)
result = loadBulkdataFile(*newElem, filePathNormalized, offset, length);
// -> file.fopen(filepath, "rb") // resolves path again independently
Reproduction
The attached IC-DCMTK-0024_toctou_poc.c is a multi-threaded PoC that races 4 threads swapping a file between regular content and a symlink to /etc/hostname, while repeatedly invoking json2dcm with +Bd (permitted bulkdata directory).
gcc -O2 -pthread -o toctou_poc IC-DCMTK-0024_toctou_poc.c
./toctou_poc /path/to/json2dcm /path/to/dicom.dic /etc/hostname 3000
Actual output:
Target: /etc/hostname -> 'w2lab-server'
RACE WON on attempt 79!
RACE WON on attempt 134!
RACE WON on attempt 645!
Result: 3 wins
The target file's content (w2lab-server) was embedded into the DICOM output, confirming the race can be won in practice (~0.1% success rate per attempt).
Fix
Use O_NOFOLLOW with open() to reject symlinks, preventing path traversal via BulkDataURI:
int fd = open(filepath.c_str(), O_RDONLY | O_NOFOLLOW);
if (fd < 0) {
// ELOOP means symlink was encountered
return EC_InvalidFilename;
}
FILE *f = fdopen(fd, "rb");