feat(security): add global toggle for private/internal URL resolution#14054
Closed
kshitijk4poor wants to merge 1 commit into
Closed
feat(security): add global toggle for private/internal URL resolution#14054kshitijk4poor wants to merge 1 commit into
kshitijk4poor wants to merge 1 commit into
Conversation
…ution On networks using OpenWrt routers, corporate proxies, or VPNs that resolve external domains to private IP ranges (198.18.0.0/15, 100.64.x, etc.), Hermes blocks ALL outbound requests because is_safe_url() treats any private IP as an SSRF attack vector. This adds a global toggle that disables private-IP blocking across all 23 call sites (web_tools, vision_tools, browser_tool, and 13 gateway platform adapters) from a single config key. Security guarantee: cloud metadata endpoints are ALWAYS blocked regardless of the toggle: - Hostnames: metadata.google.internal, metadata.goog - IPs: 169.254.169.254, fd00:ec2::254 Three ways to enable (priority order): 1. HERMES_ALLOW_PRIVATE_URLS=true env var 2. security.allow_private_urls: true in config.yaml 3. browser.allow_private_urls: true (legacy, now promotes globally) Files changed: - tools/url_safety.py: _global_allow_private_urls() with cached config read, _ALWAYS_BLOCKED_IPS for metadata endpoints, is_safe_url() checks the toggle after blocking metadata hostnames - hermes_cli/config.py: security.allow_private_urls in DEFAULT_CONFIG - tests/tools/test_url_safety.py: 32 new tests covering toggle, config fallback, caching, and security invariants (metadata always blocked)
626b6db to
762e107
Compare
Contributor
|
Merged via #14166 — your commit was cherry-picked onto current main with authorship preserved in git log. Had to drop the stray |
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Users on networks where DNS resolves external domains to private IP ranges are completely blocked from using Hermes's web tools, browser, vision URL fetching, and gateway media downloads. This affects:
198.18.0.0/15(IANA benchmarking range) for DNS resolution100.64.0.0/10(CGNAT range)Example from a user's environment —
nousresearch.comresolves to a private IP:Python's
ipaddress.is_privatereturnsTruefor198.18.23.183(it's in the IANA benchmarking range198.18.0.0/15), so Hermes's SSRF guard blocks the request with:This affects all 23 call sites that use
is_safe_url()— web_extract, web_crawl, browser_navigate, vision_analyze URL downloads, and media downloads across all 13 gateway platform adapters (Telegram, Discord, Slack, Matrix, Feishu, etc.).Previously, only the browser tool had an escape hatch (
browser.allow_private_urlsin config.yaml), but the other 21 call sites had no bypass mechanism at all.Competitor Analysis
Audited OpenCode, Cline, and Claude Code (closed source). None of them have any SSRF protection:
We're already well ahead. This PR makes the protection configurable without weakening the security-critical parts.
Solution
A single global toggle in
tools/url_safety.pythat all 23 call sites inherit automatically — no changes needed at individual call sites.Configuration (three ways, in priority order):
HERMES_ALLOW_PRIVATE_URLS=truesecurity.allow_private_urls: truein~/.hermes/config.yamlbrowser.allow_private_urls: true(existing key, now promotes globally)Security guarantees preserved — hardened after competitive review
When the toggle is enabled, cloud metadata endpoints and the entire link-local range are ALWAYS blocked:
metadata.google.internalmetadata.goog169.254.169.254169.254.170.2169.254.169.253169.254.0.0/16(entire range)fd00:ec2::254100.100.100.200192.168.1.1198.18.23.183100.64.0.1Key design: hostname checks (
metadata.google.internal,metadata.goog) run before DNS resolution, so on networks where these hostnames resolve to local proxy IPs, they're still blocked by the string match.Files changed
tools/url_safety.py— Core change:_global_allow_private_urls()with cached config read, expanded_ALWAYS_BLOCKED_IPS(5 cloud metadata IPs), new_ALWAYS_BLOCKED_NETWORKS(entire 169.254.0.0/16),is_safe_url()checks toggle after blocking metadatahermes_cli/config.py—security.allow_private_urls: falseadded toDEFAULT_CONFIGtests/tools/test_url_safety.py— 37 new tests across 3 test classesTests
New test coverage:
TestGlobalAllowPrivateUrls(11 tests) — toggle defaults, env var parsing, config.yaml security + browser fallback, precedence, cachingTestAllowPrivateUrlsIntegration(16 tests) — fullis_safe_url()integration: private/benchmark/CGNAT/localhost IPs allowed with toggle, and all metadata endpoints always blocked: AWS (169.254.169.254), ECS (169.254.170.2), Azure wire server (169.254.169.253), Alibaba (100.100.100.200), AWS IPv6 (fd00:ec2::254), full link-local range (169.254.x.x), metadata hostnames, DNS failures, empty URLsHow to use
Users hitting this issue add one line to
~/.hermes/config.yaml:Or set the env var
HERMES_ALLOW_PRIVATE_URLS=truefor a quick workaround.