Skip to content

security: validate uploaded image bytes before decoding (decompression bomb) - #13

Merged
IbraTech04 merged 1 commit into
minbar-featuresfrom
sec/unit3-validate-image-bytes
Aug 30, 2026
Merged

IbraTech04 merged 1 commit into
minbar-featuresfrom
sec/unit3-validate-image-bytes

Conversation

@IbraTech04

Copy link
Copy Markdown
Collaborator

The problem

Nothing ever looked at the bytes of an uploaded file.

  • UploadWorkflowService.verifyIntegrity checked the declared size against R2's stored Content-Length and a client-supplied SHA-256. Both measure length, neither inspects content.
  • UploadLimitsService.validateUpload matched contentType — a client-supplied string — against uploadProperties.getAllowedFileTypes().
  • The object then went straight into Thumbnails.of(new ByteArrayInputStream(originalBytes)) (Thumbnailator → ImageIO). ImageProcessingProperties bounded only the output thumbnail; nothing bounded the input.

Two consequences. A file could declare image/png, be a ZIP, and reach the decoder. And a PNG whose IHDR advertises an enormous resolution could be a few hundred bytes on the wire while asking ImageIO to allocate a multi-gigabyte raster on the async thumbnail pool — a classic decompression bomb, reachable via presign → PUT → complete because the size and hash checks both pass.

The fix

A new ImageValidationService:

  1. Sniffs the container from magic bytes (JPEG, PNG, GIF87a/89a, WEBP via RIFF, HEIC via ftyp brand), requires the result to be on the allowlist, and requires it to match what the client declared.
  2. Reads width and height off the header via ImageIO.getImageReaders / getWidth(0) / getHeight(0) and rejects anything above a configurable megapixel ceiling (imageprocessing.max-megapixels, default 50) — before any decode happens.

No new dependency: the JDK's own readers plus a small signature table are enough, so no Tika. The rejection message names only the declared type — echoing the sniffed one back would tell a prober exactly which signatures the server recognises.

Verification

ImageValidationServiceTest   tests=8  failures=0  errors=0
---
Tests run: 583, Failures: 0, Errors: 0, Skipped: 0     BUILD SUCCESS

The bomb fixture is the real shape, not a mock: a valid PNG header advertising 40000×40000 (1.6 gigapixels) in under 1 KB, asserted to be rejected without its image data ever being decoded. Also covered: a ZIP declared as PNG, a genuine PNG declared as JPEG, empty and truncated input, that the message does not leak the sniffed type, and that the ceiling is actually configurable (4 MP passes at 50 MP, fails at 1 MP).

Verified by test rather than the live presign → PUT → complete flow, which needs a reachable R2 bucket this machine does not have.

🤖 Generated with Claude Code

https://claude.ai/code/session_011xQusSt39vP3c82gAUXRCm

verifyIntegrity checked only the declared size against R2's stored
Content-Length plus a client-supplied SHA-256, and validateUpload
matched contentType -- a client-supplied string -- against the
allowlist. Nothing ever looked at the bytes. The object then went
straight into Thumbnails.of(...) (Thumbnailator -> ImageIO), and
ImageProcessingProperties bounded only the OUTPUT thumbnail, so a small
file advertising an enormous resolution in its header could ask ImageIO
to allocate a multi-gigabyte raster on the async pool.

New ImageValidationService sniffs the container from magic bytes
(JPEG/PNG/GIF/WEBP/HEIC), requires it to be on the allowlist AND to
match what the client declared, then reads width and height off the
header via ImageIO.getImageReaders and rejects anything above a
configurable megapixel ceiling (default 50) -- all before any decode
happens. No new dependency; the JDK's readers plus a small signature
table are enough.

The rejection message names only the declared type: echoing the sniffed
one back would tell a prober which signatures the server recognises.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011xQusSt39vP3c82gAUXRCm
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@IbraTech04

Copy link
Copy Markdown
Collaborator Author

This is fine and can be merged safely

@IbraTech04
IbraTech04 merged commit 7c42b6d into minbar-features Aug 30, 2026
1 check failed
@IbraTech04
IbraTech04 deleted the sec/unit3-validate-image-bytes branch August 30, 2026 04:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant