Web 工具:AI 如何访问互联网

深入 Claude Code 的网络访问工具——WebFetchTool 内容获取、WebSearchTool 搜索、代理配置、安全考量

问题引入

AI 编码助手的知识有一个天然的截止日期——模型训练数据的时间点。当用户问"React 19 的新 API 怎么用"或"这个 npm 包最新版有什么 breaking changes"时,AI 只能靠互联网访问来获取最新信息。

但让 AI 访问互联网带来了新的安全挑战:

  1. SSRF(Server-Side Request Forgery) — AI 可能被注入恶意 URL,访问内网服务
  2. 数据外泄 — 恶意网页可能指示 AI 将用户代码发送到外部
  3. Token 炸弹 — 一个巨大的网页可能消耗全部上下文空间
  4. 认证泄露 — 如果 AI 带着用户的 cookie 或 token 访问网页,可能泄露凭据

Claude Code 通过两个专用工具解决这些问题:WebFetchTool(获取指定 URL 的内容)和 WebSearchTool(搜索互联网)。在 CCR(Claude Code Remote)环境中,还有一个上游代理层提供额外的网络控制。


WebFetchTool:内容获取

WebFetchTool 从指定 URL 获取内容,并让 AI 用自然语言 prompt 处理获取到的内容。

输入模型

src/tools/WebFetchTool/WebFetchTool.ts:24-30
TypeScript
24const inputSchema = lazySchema(() =>
25 z.strictObject({
26 url: z.string().url().describe('The URL to fetch content from'),
27 prompt: z.string().describe('The prompt to run on the fetched content'),
28 }),
29)

两个参数:urlpromptprompt 的设计意图是让 AI 不仅仅获取原始内容,而是带着目的去提取信息。例如:"从这个 API 文档中提取所有 endpoint 和它们的参数"。

输出包含 HTTP 状态码、处理后的文本、获取时间和内容大小:

src/tools/WebFetchTool/WebFetchTool.ts:32-45
TypeScript
32const outputSchema = lazySchema(() =>
33 z.object({
34 bytes: z.number().describe('Size of the fetched content in bytes'),
35 code: z.number().describe('HTTP response code'),
36 codeText: z.string().describe('HTTP response code text'),
37 result: z.string().describe('Processed result from applying the prompt'),
38 durationMs: z.number().describe('Time taken to fetch and process'),
39 url: z.string().describe('The URL that was fetched'),
40 }),
41)

预批准域名白名单

WebFetchTool 最重要的安全机制之一是预批准域名列表:

src/tools/WebFetchTool/preapproved.ts:14-131
TypeScript
14export const PREAPPROVED_HOSTS = new Set([
15 // Anthropic
16 'platform.claude.com',
17 'code.claude.com',
18 'modelcontextprotocol.io',
19
20 // Top Programming Languages
21 'docs.python.org',
22 'en.cppreference.com',
23 'developer.mozilla.org',
24 'doc.rust-lang.org',
25 'www.typescriptlang.org',
26
27 // Web Frameworks
28 'react.dev',
29 'nextjs.org',
30 'vuejs.org',
31 'tailwindcss.com',
32
33 // Cloud & DevOps
34 'docs.aws.amazon.com',
35 'cloud.google.com',
36 'kubernetes.io',
37
38 // ... 100+ domains total
39])

这些域名可以无需用户确认即可访问。列表的选择标准是"代码相关的文档站点"——它们是只读的参考资料,不涉及认证或用户数据。

注意源码中的安全警告:

Text
1// SECURITY WARNING: These preapproved domains are ONLY for WebFetch (GET requests only).
2// The sandbox system deliberately does NOT inherit this list for network restrictions,
3// as arbitrary network access (POST, uploads, etc.) to these domains could enable
4// data exfiltration. Some domains like huggingface.co, kaggle.com, and nuget.org
5// allow file uploads and would be dangerous for unrestricted network access.

这是一个关键的安全区分:WebFetch 只做 GET 请求(只读),而沙箱的网络限制控制的是任意网络操作(包括 POST)。两者不能共享白名单。

路径级别的预批准

src/tools/WebFetchTool/preapproved.ts:136-166
TypeScript
136const { HOSTNAME_ONLY, PATH_PREFIXES } = (() => {
137 const hosts = new Set<string>()
138 const paths = new Map<string, string[]>()
139 for (const entry of PREAPPROVED_HOSTS) {
140 const slash = entry.indexOf('/')
141 if (slash === -1) {
142 hosts.add(entry)
143 } else {
144 const host = entry.slice(0, slash)
145 const path = entry.slice(slash)
146 const prefixes = paths.get(host)
147 if (prefixes) prefixes.push(path)
148 else paths.set(host, [path])
149 }
150 }
151 return { HOSTNAME_ONLY: hosts, PATH_PREFIXES: paths }
152})()
153
154export function isPreapprovedHost(hostname: string, pathname: string): boolean {
155 if (HOSTNAME_ONLY.has(hostname)) return true
156 const prefixes = PATH_PREFIXES.get(hostname)
157 if (prefixes) {
158 for (const p of prefixes) {
159 // Enforce path segment boundaries
160 if (pathname === p || pathname.startsWith(p + '/')) return true
161 }
162 }
163 return false
164}

某些域名只对特定路径预批准。例如 github.com/anthropics 是预批准的,但 github.com/random-user 不是。路径匹配强制要求段边界(/),防止 /anthropics-evil/malware 被误匹配。

数据结构在模块加载时预处理为两个查找表(HOSTNAME_ONLY Set 和 PATH_PREFIXES Map),使运行时匹配为 O(1)。

权限检查流程

...

权限规则以 domain:hostname 格式存储。当用户批准访问某个域名时,该域名的所有 URL 都被批准。

Prompt 中的认证警告

src/tools/WebFetchTool/WebFetchTool.ts:181-189
TypeScript
181 async prompt(_options) {
182 return `IMPORTANT: WebFetch WILL FAIL for authenticated or private URLs. Before using this tool, check if the URL points to an authenticated service (e.g. Google Docs, Confluence, Jira, GitHub). If so, look for a specialized MCP tool that provides authenticated access.
183${DESCRIPTION}`
184 },

这个警告始终包含在 prompt 中,不管 ToolSearchTool 是否可用。源码注释解释了原因:如果这个前缀根据 ToolSearch 可用性有条件地切换,会导致工具描述在连续的 API 调用间"闪烁"(flicker),破坏 Anthropic API 的 prompt 缓存——每次闪烁都意味着两次缓存未命中。


WebSearchTool:互联网搜索

WebSearchTool 使用 Anthropic 的 Web Search API 进行互联网搜索。与 WebFetchTool 不同,它不是获取特定 URL,而是搜索整个互联网。

架构特殊性

WebSearchTool 不是简单地调用搜索 API——它是一个模型套模型的架构:

src/tools/WebSearchTool/WebSearchTool.ts:254-291
TypeScript
254 async call(input, context, _canUseTool, _parentMessage, onProgress) {
255 const { query } = input
256 const userMessage = createUserMessage({
257 content: 'Perform a web search for the query: ' + query,
258 })
259 const toolSchema = makeToolSchema(input)
260
261 const queryStream = queryModelWithStreaming({
262 messages: [userMessage],
263 systemPrompt: asSystemPrompt([
264 'You are an assistant for performing a web search tool use',
265 ]),
266 tools: [],
267 signal: context.abortController.signal,
268 options: {
269 extraToolSchemas: [toolSchema],
270 querySource: 'web_search_tool',
271 // ...
272 },
273 })
274 // ...
275 }

它创建一个内部的 API 调用,传入 web_search_20250305 类型的工具 Schema。API 端会自动执行搜索并返回结果。这种架构的好处是:搜索的实际执行由 Anthropic 的基础设施处理,客户端只需要处理流式响应。

搜索限制

src/tools/WebSearchTool/WebSearchTool.ts:76-84
TypeScript
76function makeToolSchema(input: Input): BetaWebSearchTool20250305 {
77 return {
78 type: 'web_search_20250305',
79 name: 'web_search',
80 allowed_domains: input.allowed_domains,
81 blocked_domains: input.blocked_domains,
82 max_uses: 8, // Hardcoded to 8 searches maximum
83 }
84}

每次调用最多执行 8 次搜索。allowed_domainsblocked_domains 允许 AI 控制搜索范围——例如只搜索官方文档站点,或排除已知的低质量结果源。

提供商可用性

src/tools/WebSearchTool/WebSearchTool.ts:169-193
TypeScript
169 isEnabled() {
170 const provider = getAPIProvider()
171 const model = getMainLoopModel()
172
173 if (provider === 'firstParty') return true
174
175 if (provider === 'vertex') {
176 const supportsWebSearch =
177 model.includes('claude-opus-4') ||
178 model.includes('claude-sonnet-4') ||
179 model.includes('claude-haiku-4')
180 return supportsWebSearch
181 }
182
183 if (provider === 'foundry') return true
184
185 return false
186 },

WebSearchTool 只在支持 Web Search API 的提供商上可用:Anthropic 第一方、Google Vertex(仅 Claude 4.0+ 模型)和 Foundry。

进度报告

src/tools/WebSearchTool/WebSearchTool.ts:298-388
TypeScript
298 for await (const event of queryStream) {
299 // Track tool use ID when server_tool_use starts
300 if (event.type === 'stream_event' &&
301 event.event?.type === 'content_block_start') {
302 const contentBlock = event.event.content_block
303 if (contentBlock?.type === 'server_tool_use') {
304 currentToolUseId = contentBlock.id
305 currentToolUseJson = ''
306 }
307 }
308
309 // Accumulate JSON for current tool use
310 if (currentToolUseId &&
311 event.type === 'stream_event' &&
312 event.event?.type === 'content_block_delta') {
313 const delta = event.event.delta
314 if (delta?.type === 'input_json_delta' && delta.partial_json) {
315 currentToolUseJson += delta.partial_json
316 // Try to extract query from partial JSON for progress updates
317 // ...
318 }
319 }
320
321 // Yield progress when search results come in
322 if (event.type === 'stream_event' &&
323 event.event?.type === 'content_block_start') {
324 const contentBlock = event.event.content_block
325 if (contentBlock?.type === 'web_search_tool_result') {
326 // Report progress
327 if (onProgress) {
328 onProgress({
329 toolUseID: toolUseId,
330 data: { type: 'search_results_received', resultCount, query },
331 })
332 }
333 }
334 }
335 }

WebSearchTool 在搜索过程中通过 onProgress 回调报告进度。由于搜索是流式的,它可以在搜索结果到达时实时更新 UI,而不是等所有搜索完成后才返回。


上游代理(Upstream Proxy)

在 CCR(Claude Code Remote)环境中,所有网络流量通过上游代理路由,提供额外的安全控制。

初始化流程

sequenceDiagram
    participant CLI as Claude Code
    participant Token as /run/ccr/session_token
    participant API as Anthropic API
    participant Relay as 本地 Relay

    CLI->>Token: 读取会话 Token
    Token-->>CLI: session_token

    CLI->>CLI: prctl(PR_SET_DUMPABLE, 0)<br>阻止 ptrace 读取堆内存

    CLI->>API: GET /v1/code/upstreamproxy/ca-cert
    API-->>CLI: CA 证书

    CLI->>CLI: 合并系统 CA + 代理 CA

    CLI->>Relay: 启动 CONNECT-to-WebSocket relay
    Relay-->>CLI: 监听 127.0.0.1:PORT

    CLI->>Token: unlink(session_token)<br>令牌仅存于堆内存

    CLI->>CLI: 设置环境变量<br>HTTPS_PROXY, SSL_CERT_FILE
src/upstreamproxy/upstreamproxy.ts:79-153
TypeScript
79export async function initUpstreamProxy(opts?) {
80 if (!isEnvTruthy(process.env.CLAUDE_CODE_REMOTE)) return state
81 if (!isEnvTruthy(process.env.CCR_UPSTREAM_PROXY_ENABLED)) return state
82
83 const token = await readToken(tokenPath)
84 if (!token) return state
85
86 setNonDumpable()
87
88 const caOk = await downloadCaBundle(baseUrl, systemCaPath, caBundlePath)
89 if (!caOk) return state
90
91 try {
92 const relay = await startUpstreamProxyRelay({ wsUrl, sessionId, token })
93 registerCleanup(async () => relay.stop())
94 state = { enabled: true, port: relay.port, caBundlePath }
95
96 // Only unlink after the listener is up
97 await unlink(tokenPath).catch(() => {})
98 } catch (err) {
99 // Fail open — a broken proxy must never break a session
100 }
101
102 return state
103}

关键安全措施:

  1. prctl 防护PR_SET_DUMPABLE=0 阻止同 UID 的进程通过 ptrace 读取此进程的堆内存。这防止了 prompt 注入攻击通过 gdb -p $PPID 窃取会话 token

  2. Token 文件删除 — token 在 relay 启动成功后从磁盘删除,仅保留在进程内存中。删除只在 relay 确认可用后执行,以便 supervisor 在失败时可以用磁盘上的 token 重试

  3. 失败开放 — 任何步骤的失败都只是禁用代理,不会中断会话。注释说得很清楚:"A broken proxy setup must never break an otherwise-working session."

NO_PROXY 列表

src/upstreamproxy/upstreamproxy.ts:37-63
TypeScript
37const NO_PROXY_LIST = [
38 'localhost', '127.0.0.1', '::1',
39 '169.254.0.0/16', // Link-local
40 '10.0.0.0/8', // RFC1918
41 '172.16.0.0/12',
42 '192.168.0.0/16',
43
44 // Anthropic API — three forms because NO_PROXY parsing differs:
45 'anthropic.com', // apex domain fallback
46 '.anthropic.com', // Python urllib/httpx (suffix match)
47 '*.anthropic.com', // Bun, curl, Go (glob match)
48
49 'github.com',
50 'registry.npmjs.org',
51 'pypi.org',
52].join(',')

Anthropic API 使用三种格式的同一域名,因为不同的运行时(Bun、Python、Go)解析 NO_PROXY 的方式不同。这种防御性编程确保 Anthropic API 请求永远不会通过上游代理,避免了 MITM 代理的伪造 CA 破坏非 Bun 运行时的 HTTPS 验证。

环境变量传播

src/upstreamproxy/upstreamproxy.ts:160-199
TypeScript
160export function getUpstreamProxyEnv(): Record<string, string> {
161 if (!state.enabled || !state.port || !state.caBundlePath) {
162 // If we inherited proxy vars from the parent, pass them through
163 if (process.env.HTTPS_PROXY && process.env.SSL_CERT_FILE) {
164 const inherited: Record<string, string> = {}
165 for (const key of ['HTTPS_PROXY', 'https_proxy', 'NO_PROXY', 'no_proxy',
166 'SSL_CERT_FILE', 'NODE_EXTRA_CA_CERTS', 'REQUESTS_CA_BUNDLE',
167 'CURL_CA_BUNDLE']) {
168 if (process.env[key]) inherited[key] = process.env[key]
169 }
170 return inherited
171 }
172 return {}
173 }
174 const proxyUrl = `http://127.0.0.1:${state.port}`
175 return {
176 HTTPS_PROXY: proxyUrl,
177 https_proxy: proxyUrl, // lowercase for Python
178 NO_PROXY: NO_PROXY_LIST,
179 no_proxy: NO_PROXY_LIST, // lowercase for Python
180 SSL_CERT_FILE: state.caBundlePath,
181 NODE_EXTRA_CA_CERTS: state.caBundlePath,
182 REQUESTS_CA_BUNDLE: state.caBundlePath, // Python requests
183 CURL_CA_BUNDLE: state.caBundlePath, // curl
184 }
185}

代理环境变量以多种格式设置,覆盖不同的客户端库:

  • HTTPS_PROXY / https_proxy — 大小写两种形式(Node.js 用大写,Python 用小写)
  • SSL_CERT_FILE — OpenSSL 通用
  • NODE_EXTRA_CA_CERTS — Node.js 专用
  • REQUESTS_CA_BUNDLE — Python requests 库
  • CURL_CA_BUNDLE — curl 命令

子进程(Bash、MCP、LSP、Hooks)都通过 subprocessEnv() 继承这些变量。


安全考量总结

Web 工具安全层
预批准域名白名单
100+ 代码文档站点 · 仅限 GET 请求
权限系统
domain:hostname 规则 · allow/deny/ask
URL 验证
必须是有效 URL · 拒绝无效格式
认证警告
Prompt 中明确 · WebFetch 不支持认证
沙箱网络限制
独立于 WebFetch 白名单 · 控制所有网络操作
上游代理 (CCR)
HTTPS MITM · Token 保护 + prctl

六层安全防护,从最宽松(预批准白名单自动通过)到最严格(上游代理 MITM 拦截),形成了纵深防御。

关键的安全隔离:WebFetch 白名单 =/= 沙箱网络白名单。huggingface.co 可能是读取文档的安全来源(WebFetch),但允许它通过沙箱进行任意网络操作就可能成为数据外泄通道(支持文件上传)。


设计启示

Claude Code 的 Web 工具设计体现了几个核心原则:

  1. 最小权限 — 默认不允许访问任何域名,只有代码文档站点被预批准,其他需要用户明确授权

  2. 安全隔离 — WebFetch(只读 GET)和沙箱网络(任意操作)有独立的白名单,WebSearch 不需要白名单(由 API 端控制)

  3. 失败开放 vs 失败关闭 — 上游代理失败时开放(不阻断会话),但权限检查失败时关闭(阻止访问)。这反映了不同组件的风险等级

  4. 多运行时兼容 — 环境变量、NO_PROXY 格式、CA 证书路径——每个网络配置都考虑了 Bun/Node.js/Python/curl 的差异