feat: opt-in block_internal_urls egress filter for the library layer (Refs #2146) - #2151
Open
joysinleung wants to merge 1 commit into
Open
feat: opt-in block_internal_urls egress filter for the library layer (Refs #2146)#2151joysinleung wants to merge 1 commit into
block_internal_urls egress filter for the library layer (Refs #2146)#2151joysinleung wants to merge 1 commit into
Conversation
Implements the opt-in library-side egress filtering discussed in unclecode#2146. - New crawl4ai/url_safety.py: dependency-free SSRF guard reusing the same blocked address ranges as deploy/docker, with BlockedURLError and an opaque error message (no DNS-oracle leak). Honors CRAWL4AI_ALLOW_INTERNAL_URLS. - CrawlerRunConfig.block_internal_urls (default False) to preserve the user-agent contract. - Chokepoint inserted in AsyncCrawlerStrategy.crawl() (HTTP path) and AsyncPlaywrightCrawlerStrategy.crawl() (browser path). - Tests in tests/async/test_url_safety.py (23 passed).
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.
Summary
Implements the opt-in library-side egress filter discussed in #2146. It started life as a responsible-disclosure report (the library fetches any caller-supplied URL, including internal/cloud-metadata addresses), and the maintainers classified that as intended behavior for a user-agent invoked by a trusted caller — the SSRF trust boundary correctly lives at the Docker API server (
egress_broker.py). The one scenario that justifies exposing the same primitives to library callers is agentic / LLM-chosen-URL pipelines, where a prompt-injection or malicious page can steer the crawler at internal hosts before the embedding app validates the URL.This PR adds an opt-in
block_internal_urlsflag so those callers get server-grade destination filtering without re-implementing it.Design
New module
crawl4ai/url_safety.py(dependency-free — no FastAPI, no egress proxy, so it imports cleanly from the library):deploy/docker/utils.py(_BLOCKED_NETWORKS: loopback, private, link-local incl.169.254.0.0/16cloud metadata, CGN100.64/10,0.0.0.0/8, IPv6 ULA/link-local/loopback) plus a_BLOCKED_HOSTNAMESset (localhost,metadata,metadata.google.internal,kubernetes.default*).BlockedURLError(ValueError)with an opaque message that never echoes the resolved IP/hostname (no DNS-oracle leak).::ffff:127.0.0.1,::127.0.0.1) before checking, mirroring the Docker server.CRAWL4AI_ALLOW_INTERNAL_URLSescape hatch (parity with the server).raw:/raw://URLs (inline HTML, no network fetch) are never treated as internal.Config:
CrawlerRunConfig.block_internal_urls(defaultFalse) — preserves the user-agent contract; existing behavior is unchanged. Becausefrom_kwargs/to_dict/cloneare signature-driven, the field flows through serialization automatically.Chokepoint: inserted in both crawl entry points right after the scheme allow-list check:
AsyncCrawlerStrategy.crawl()→ covers the HTTP path (AsyncHTTPCrawlerStrategy._handle_http).AsyncPlaywrightCrawlerStrategy.crawl()→ covers the browser path (page.goto).Only
http(s)://URLs are checked;file://andraw:are local/inline content and are skipped.Tests
tests/async/test_url_safety.py(23 tests, all passing). Deterministic (IP literals, no DNS dependency):is_internal_url/check_url_destinationclassification for internal vs global addresses (incl. IPv6, CGN, metadata).BlockedURLErrorraised on internal, passes on global.CRAWL4AI_ALLOW_INTERNAL_URLSoverride.CrawlerRunConfigdefault / set /to_dictroundtrip /clone.AsyncHTTPCrawlerStrategyproves the chokepoint actually fires —block_internal_urls=Trueraises before any connection,False(default) fetches and returns content.Answers to the open questions from #2146
block_internal_urls(matches the existingcheck_robots_txt/prefetchstyle onCrawlerRunConfig). Happy to rename if you preferdeny_private_destinations/egress_filter.url_safety.py; it's called from bothcrawl()entry points so HTTP and browser paths stay in sync (no duplicated trust logic).gotofollows redirects internally and the library connects directly (no pinning proxy), so redirect hops are not revalidated in this opt-in layer. That gap is exactly why the authoritative trust boundary for untrusted multi-tenant input remains the Docker server'segress_broker(DNS pinning + per-hop revalidation). I noted this as a deliberate limitation rather than silently claiming full coverage — if you'd like, a follow-up could add an opt-in "disable browser redirects + manual revalidate" mode.Scope note
This is not a default-behavior change and does not close the original SSRF report's premise — it's the opt-in escape hatch the maintainers offered. Default
arun(url)behavior is unchanged.Refs #2146.