Skip to content

fix(security): image format validation with magic bytes, size limits, and SVG blocking - #2690

Closed
0xbyt4 wants to merge 3 commits into
NousResearch:mainfrom
0xbyt4:fix/vision-image-validation
Closed

fix(security): image format validation with magic bytes, size limits, and SVG blocking#2690
0xbyt4 wants to merge 3 commits into
NousResearch:mainfrom
0xbyt4:fix/vision-image-validation

Conversation

@0xbyt4

@0xbyt4 0xbyt4 commented Mar 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Vision tools and platform image cache accepted any file as an image based on URL extension alone. A .jpg URL serving HTML, scripts, or executables was downloaded, cached, and sent to the LLM without content validation.

What was wrong

  • No format validation: MIME type determined from file extension, not content. A .jpg file containing <script>alert(1)</script> was accepted as image/jpeg.
  • No size limit: Arbitrarily large files downloaded into memory and disk.
  • SVG allowed: SVG can contain <script>, <iframe>, event handlers.
  • Platform cache unprotected: cache_image_from_bytes() / cache_image_from_url() in base.py had zero validation — images from Telegram, Discord, Signal, WhatsApp, Matrix, Mattermost, Slack, Email all bypassed any check.
  • No SSRF on cache path: cache_image_from_url() had no is_safe_url() check.
  • Local files skipped: vision_analyze_tool skipped validation for local file paths.

Fix

New module tools/image_safety.py with:

  • Magic-byte validation via filetype library (pure Python, 50KB, no system deps)
  • 9 supported formats: JPEG, PNG, GIF, WebP, BMP, TIFF, HEIC, HEIF, AVIF
  • SVG blocked (script injection risk)
  • 20MB size limit (Content-Length pre-check + post-download file size check)
  • Graceful fallback if filetype unavailable (extension-only with warning)

Applied to ALL image entry points:

  • vision_tools._download_image() — remote URL downloads
  • vision_tools.vision_analyze_tool() — local file paths
  • base.cache_image_from_bytes() — all messaging platform image cache
  • base.cache_image_from_url() — URL-based cache + SSRF protection

Protected platforms

Telegram, Discord, Signal, WhatsApp, Matrix, Mattermost, Slack, Email, CLI — all use cache_image_from_bytes or cache_image_from_url which now validate.

Test plan

  • 68 tests pass (46 existing vision + 22 new image_safety)
  • Real images accepted: JPEG, PNG, GIF, WebP
  • Fake images rejected: HTML-as-jpg, ELF-as-jpg, PDF-as-png
  • SVG/SVGZ blocked by extension
  • Platform cache rejects non-image bytes
  • Platform cache URL SSRF blocked
  • Local file paths validated before processing
  • Size limit enforced (Content-Length + file size)
  • Full end-to-end flow verified with bash (7 scenarios)
  • Existing vision tests unaffected

0xbyt4 added 3 commits March 24, 2026 02:13
…tools

Vision tools accepted any file as an image based on URL extension alone.
A .jpg URL serving HTML, scripts, or executables was downloaded, base64
encoded, and sent to the LLM without any content validation.

Adds tools/image_safety.py with:
- Magic-byte validation via filetype library (pure Python, 50KB)
- Rejects non-image files regardless of extension (HTML, PDF, ELF, etc.)
- SVG blocked by default (can contain <script>, <iframe>, event handlers)
- 20MB size limit enforced via Content-Length pre-flight and post-download check
- MIME type detection from content instead of extension
- Graceful fallback if filetype is unavailable (extension-only with warning)

Changes to vision_tools.py:
- _download_image: Content-Length check before download, format validation after
- _validate_image_url: SVG URLs blocked at URL validation stage
- _determine_mime_type: uses magic bytes instead of file extension

22 new tests covering:
- Real images pass (JPEG, PNG, GIF, WebP)
- Fake images rejected (HTML-as-jpg, ELF-as-jpg, PDF-as-png)
- SVG/SVGZ blocked
- Empty/missing files handled
- Size limits enforced
- MIME detection from content vs extension
…le paths

cache_image_from_bytes() accepted any bytes from all messaging platforms
without format or size validation. cache_image_from_url() had no SSRF
protection. vision_analyze_tool skipped validation for local file paths.

- cache_image_from_bytes: validate magic bytes + enforce 20MB size limit
- cache_image_from_url: add is_safe_url() SSRF check before download
- vision_analyze_tool: validate local files before processing
- Fix test_oversized_file which never actually tested size rejection
@alt-glitch alt-glitch added type/security Security vulnerability or hardening P1 High — major feature broken, no workaround tool/vision Vision analysis and image generation comp/gateway Gateway runner, session dispatch, delivery labels May 3, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

Related to #3845 (merged) which addressed vision file rejection. This PR extends validation to all image entry points (platform caches, SSRF) and adds magic-byte checking.

1 similar comment
@alt-glitch

Copy link
Copy Markdown
Collaborator

Related to #3845 (merged) which addressed vision file rejection. This PR extends validation to all image entry points (platform caches, SSRF) and adds magic-byte checking.

@egilewski

Copy link
Copy Markdown
Contributor

merge conflicts

This PR does not merge cleanly with the base branch. Please rebase or merge current main and resolve the conflicts if it's still relevant.

Signed: GPT-5.5-low in Codex

@teknium1

Copy link
Copy Markdown
Contributor

Thanks for this @0xbyt4 — your threat analysis was on point, and the codebase has since converged on exactly the protections you proposed (just via independent later work, since this PR is ~10k commits behind current main). Closing as superseded, with credit for the direction.

Verified line-by-line against current main:

Your claim Now on main
Magic-byte format validation _looks_like_image() gates cache_image_from_bytes in gateway/platforms/base.py (PNG/JPEG/GIF/BMP/WebP)
Size limit validate_inbound_media_size + _read_httpx_body_with_limit on the cache path; _VISION_MAX_DOWNLOAD_BYTES Content-Length + body check in vision_tools._download_image()
Platform cache unprotected both cache_image_from_bytes and cache_image_from_url now validate
No SSRF on cache path is_safe_url() pre-check + an async redirect guard re-validating each hop on both the cache and vision download paths

On SVG blocking: this is the one item we're intentionally not doing globally. Main deliberately supports SVG as a vision input format (tools/vision_tools.py detects <svg and returns image/svg+xml), so a blanket block would be a behavior change rather than a fix. The script-injection concern you raised is real but is being handled at the narrower surface where it matters — the media relay (see #41494).

Appreciate the careful write-up and the 7-scenario E2E plan — that's the right instinct for a security PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery P1 High — major feature broken, no workaround tool/vision Vision analysis and image generation type/security Security vulnerability or hardening

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants