Repository navigation
fix(web_fetch): add SSRF guard for self-hosted providers - #3942
Conversation
2606e05 to
48b10cb
Compare
There was a problem hiding this comment.
Pull request overview
This PR hardens DeerFlow’s self-hosted web_fetch providers against SSRF by extracting the existing Browserless URL validation into a shared deerflow.community.url_safety utility and applying it consistently to both Browserless and Crawl4AI (with an explicit allow_private_addresses opt-in retained for intentional internal access).
Changes:
- Added shared URL screening utilities in
deerflow.community.url_safetyand reused them in Browserless + Crawl4AIweb_fetch. - Added/updated tests to ensure metadata/private/loopback targets are rejected by default and allowed only with explicit opt-in.
- Documented the
allow_private_addressesSSRF guard inconfig.example.yamlfor both providers.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| config.example.yaml | Documents the SSRF guard (allow_private_addresses) for Browserless and Crawl4AI web_fetch. |
| backend/tests/test_crawl4ai_tools.py | Adds SSRF regression tests for Crawl4AI web_fetch behavior. |
| backend/tests/test_browserless_client.py | Adds SSRF regression tests for Browserless web_fetch behavior. |
| backend/packages/harness/deerflow/community/url_safety.py | Introduces shared URL validation + DNS resolution helpers for SSRF screening. |
| backend/packages/harness/deerflow/community/crawl4ai/tools.py | Applies shared URL validation to Crawl4AI web_fetch with config-controlled opt-in. |
| backend/packages/harness/deerflow/community/browserless/tools.py | Replaces local SSRF validation with shared guard and applies it to Browserless web_fetch. |
| def is_blocked_address(address: ipaddress._BaseAddress) -> bool: | ||
| """Return True for addresses web tools should not reach by default.""" | ||
| return address.is_private or address.is_loopback or address.is_link_local or address.is_reserved or address.is_multicast or address.is_unspecified |
|
|
||
| normalized_host = hostname.strip().rstrip(".").lower() | ||
| if normalized_host in _BLOCKED_HOSTNAMES: | ||
| return f"Error: Refusing to {action} a private or loopback address" |
| wait_for_event = cfg.get("wait_for_event", wait_for_event) | ||
| raw_wait = cfg.get("wait_for_timeout_ms", wait_for_timeout_ms) | ||
| wait_for_timeout_ms = int(raw_wait) if not isinstance(raw_wait, int) else raw_wait | ||
| wait_for_selector = cfg.get("wait_for_selector", wait_for_selector) |
| with patch("deerflow.community.crawl4ai.tools._get_tool_config", return_value={"allow_private_addresses": True}): | ||
| result = await tools.web_fetch_tool.ainvoke("http://10.0.0.5/dashboard") |
| with patch("deerflow.community.browserless.tools._get_tool_config", return_value={"allow_private_addresses": True}): | ||
| result = await tools.web_fetch_tool.ainvoke("http://10.0.0.5/dashboard") |
fancyboi999
left a comment
There was a problem hiding this comment.
Left one concrete follow-up: the new shared SSRF guard only covers Browserless and Crawl4AI, but fastcrw.web_fetch_tool() is still documented/configured for self-hosted use and still forwards arbitrary URLs without equivalent screening. That leaves the same internal-target fetch primitive available through another current web_fetch provider, so I don't think the hardening is complete yet.
There was a problem hiding this comment.
This still leaves one self-hosted web_fetch path unprotected: fastcrw.web_fetch_tool() is documented as supporting base_url: http://localhost:3000 for self-hosting, but it still forwards arbitrary URLs straight into client.scrape(...) without the new SSRF gate. That means the attack this PR is fixing remains reachable whenever users switch to the fastCRW provider, so the hardening is incomplete for the repo's current self-hosted fetch surface. Please either extend the shared validate_public_http_url(...) guard to backend/packages/harness/deerflow/community/fastcrw/tools.py as well (and add matching tests), or narrow the PR/docs claim so it does not imply self-hosted providers are now covered broadly.
48b10cb to
52a5b0f
Compare
|
@fancyboi999 Thanks for the catch. I extended the shared SSRF guard to fastCRW web_fetch as well, including metadata/private/DNS-to-private rejection tests and the explicit allow_private_addresses opt-in path. I also updated |
Conflict resolutions: - docker/docker-compose.yaml: kept local dir-mount hot-reload hardening (103f179) for config.yaml/extensions_config.json; adopted upstream's new Redis stream-bridge service, depends_on, and env wiring. - backend/.../community/crawl4ai/* + backend/tests/test_crawl4ai_tools.py: took upstream (bytedance#3821/bytedance#3942) — our provider plus SSRF guard, timeout coercion, and expanded tests. Module is now upstream-tracked. - csrf_middleware.py/auth.py auto-merged: our bytedance#3872 fix landed upstream. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Summary
This PR hardens self-hosted
web_fetchproviders against SSRF.DeerFlow already had URL safety validation for the Browserless web tool path: it rejects loopback, private, link-local, cloud metadata, reserved, multicast, and unresolved targets by default, with an explicit
allow_private_addresses: trueopt-in for intentional internal access.However, later-added self-hosted
web_fetchproviders, especially Crawl4AI, did not reuse that protection. An attacker could potentially guide the agent into callingweb_fetchon internal or metadata endpoints such as:When Browserless/Crawl4AI runs inside the deployment network, this can turn
web_fetchinto an SSRF primitive and leak sensitive internal information.What Changed
deerflow.community.url_safety.web_fetch.web_fetch.allow_private_addresses: trueopt-in for trusted internal targets.config.example.yamlto document the SSRF guard for Browserless and Crawl4AIweb_fetch.Validation
I started the Gateway with
make gatewayand ran runtime smoke tests against a local fake Browserless/Crawl4AI service.The smoke test verified that normal public
web_fetchstill reaches the fetch service, metadata URLs are blocked before reaching the service, andallow_private_addresses: truestill permits intentional private fetches. This confirms the feature remains functional and the new validation logic is effective.Related Browserless/Crawl4AI tests and formatting/lint checks were also run successfully.