Skip to content

fix(security): add SSRF protection to vision_tools and web_tools (hardened)#2679

Merged
teknium1 merged 3 commits intomainfrom
hermes/hermes-28b19313
Mar 23, 2026
Merged

fix(security): add SSRF protection to vision_tools and web_tools (hardened)#2679
teknium1 merged 3 commits intomainfrom
hermes/hermes-28b19313

Conversation

@teknium1
Copy link
Copy Markdown
Contributor

Summary

Salvage of PR #2630 by @dieutx with security hardening on top.

Original contribution (cherry-picked with authorship preserved):

  • New tools/url_safety.py module with is_safe_url() — resolves hostnames via DNS and blocks private/internal IP ranges
  • Integration into vision_tools.py, web_tools.py (extract + both crawl paths)
  • 13 tests in test_url_safety.py, updated existing vision test

Hardening additions:

Issue Fix
Fail-open on DNS errors and exceptions Changed to fail-closed (OWASP best practice)
CGNAT range (100.64.0.0/10) not blocked Added explicit check — is_private returns False for this range
Multicast (224.0.0.0/4) not blocked Added is_multicast and is_unspecified checks
Redirect-based SSRF bypass in vision_tools Added httpx event hook that re-validates each redirect target
Parallel/Tavily extract paths unprotected Moved SSRF filter before backend dispatch
DNS rebinding (TOCTOU) Documented as known limitation (requires connection-level fix)

Verified:

  • Live PTY testing: web_extract + vision_analyze work with public URLs, correctly block localhost/169.254.169.254
  • Full test suite: 6044 passed, 0 failed
  • Alternative IP encodings (decimal, hex, octal, shortened) all caught by getaddrinfo normalization
  • IPv4-mapped IPv6 (::ffff:127.0.0.1) correctly blocked on Python 3.13

Test plan

  • 48 new/updated SSRF-specific tests (24 in test_url_safety.py, mocks in test_vision_tools.py + test_website_policy.py)
  • Parametrized _is_blocked_ip tests covering 18 blocked and 6 allowed IPs
  • All existing tests updated to mock DNS for test hostnames (fail-closed changes would otherwise block them)

dieutx and others added 3 commits March 23, 2026 15:22
Both vision_analyze and web_extract/web_crawl accept arbitrary URLs
without checking if they target private/internal network addresses.
A prompt-injected or malicious skill could use this to access cloud
metadata endpoints (169.254.169.254), localhost services, or private
network hosts.

Adds a shared url_safety.is_safe_url() that resolves hostnames and
blocks private, loopback, link-local, and reserved IP ranges. Also
blocks known internal hostnames (metadata.google.internal).

Integrated at the URL validation layer in vision_tools and before
each website_policy check in web_tools (extract, crawl).
The existing test_valid_url_with_port asserted localhost URLs pass
validation. With SSRF protection, localhost is now correctly blocked.
Update the test to verify the block, and add a separate test for
valid URLs with ports using a public hostname.
…, redirect guard

Follow-up hardening on top of dieutx's SSRF protection (PR #2630):

- Change fail-open to fail-closed: DNS errors and unexpected exceptions
  now block the request instead of allowing it (OWASP best practice)
- Block CGNAT range (100.64.0.0/10): Python's ipaddress.is_private
  does NOT cover this range (returns False for both is_private and
  is_global). Used by Tailscale/WireGuard and carrier infrastructure.
- Add is_multicast and is_unspecified checks: multicast (224.0.0.0/4)
  and unspecified (0.0.0.0) addresses were not caught by the original
  four-check chain
- Add redirect guard for vision_tools: httpx event hook re-validates
  each redirect target against SSRF checks, preventing the classic
  redirect-based SSRF bypass (302 to internal IP)
- Move SSRF filtering before backend dispatch in web_extract: now
  covers Parallel and Tavily backends, not just Firecrawl
- Extract _is_blocked_ip() helper for cleaner IP range checking
- Add 24 new tests (CGNAT, multicast, IPv4-mapped IPv6, fail-closed
  behavior, parametrized blocked/allowed IP lists)
- Fix existing tests to mock DNS resolution for test hostnames
@teknium1 teknium1 merged commit 0791efe into main Mar 23, 2026
1 check passed
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.

2 participants