// blog
We Tried to Break Our Own DICOM Upload: What Testing With Hostile Files Found
To test a file upload, feed it the files an attacker or a broken export would: empty and non-matching files, length fields that point far past the end, structures nested thousands deep, compressed data that expands into gigabytes, markup in text fields, and many randomly damaged copies of real files. Each must be refused with a clear reason, quickly, without freezing the page or reserving huge memory. Run the random part in a separate process with a time limit so a hang fails the test.
Our DICOM viewer reads files people pick from their own computer. It runs in their browser, so a bad file can’t touch our server. It can still freeze the tab, eat gigabytes of memory, or show a cryptic error to someone who just wanted to see a scan. And once a page accepts files, people will try to break it.
So before anyone else could, we tried.
Rule one: every refusal is an Error with a reason
The reader may accept a file or refuse it. Refusing is fine. What isn’t fine: hanging, crashing, reserving memory a header asked for, or letting a raw engine error through. Every test checks that a bad file is refused with a sentence a person can read.
What we threw at it
| Attack | What should happen |
|---|---|
| Not DICOM: empty files, one byte, text, a PNG, a ZIP, random bytes | “not a DICOM file” |
| A length field claiming 4 GB | Refused before reading past the end |
| Sequences nested 5,000 deep | Refused at 64 levels, the default limit the OFFIS DICOM Toolkit (DCMTK) uses |
| A 65,535 × 65,535 image, or 100 million frames | Refused before any memory is reserved |
| A file that inflates to 620 MB | Stopped while decompressing, at a fixed limit |
| An RLE frame claiming 4 billion segments | Refused: the count must match the image |
| HTML and script in a tag | Read as text, escaped when shown |
The principle behind most of these: check everything that sizes an allocation before allocating. A header is a claim, not a fact.
Then: 1,400 random ones
The test takes eight real files, made with DCMTK in every format the reader supports, and damages each 150 times in five ways: flipped bits, a cut-off end, a huge length written over a header field, scrambled header bytes, and junk inserted in the middle. Then 200 files of pure random bytes on top.
Each must be read or refused, in time. The random generator has a fixed seed, so a failure can be replayed. And the whole run happens in a child process with a time limit: a hang can’t freeze the test, it fails it.
Result: 1,400 files, every one refused or read in a few milliseconds, about 145 MB of memory for the whole run.
The two bugs it caught
- A damaged deflate stream made the browser’s decompressor throw a bare error with no message. A visitor would have seen nothing useful. Now it says the compressed data is damaged.
- The tag table’s folding tripped over the way
dcmdumpindents sequence items, which broke the page build. The fix tracks open sequences by depth.
Neither would have shown up on the happy path. Both were fixed the same day.
Correct, too
Refusing bad files is half the job. The same tests read the good ones in every supported format and compare the pixels, one by one, with what DCMTK itself exported. They have to match exactly.
The full architecture is in the case study. Try to break it yourself.
Building something that accepts files from the public? I can help you test it.

