AI Agent 的 fetch(url) 工具構成 SSRF 攻擊面:不依賴模型的防護措施
An AI Agent's fetch(url) Tool Is an SSRF Surface: Controls That Do Not Rely on the Model
文章指出,讓 AI Agent 按 URL 讀取網頁的工具會令伺服器向模型指定的目的地發出請求,形成 SSRF 攻擊面,並整理不依賴模型判斷的防護措施。對只需存取少數已知目的地的工具,建議使用允許清單並由伺服器按識別碼組裝請求;如需抓取任意公開網頁,則要在連線時檢查解析後的 IP、拒絕自動跟隨重新導向,並配合網絡出口限制及最小權限。
A tool that takes a URL and returns the page is one of the most common additions to an AI agent. It is also a server-side request whose destination is chosen by a model, and the model reads content that someone else wrote: web pages, support tickets, documents, e-mail. When that content says "fetch this address next", the request leaves from the server's network position, not from the author's.
The question for a review is not "can the model be made to behave". It is "what can this tool reach if it is asked to reach anything".
Where the risk is classified
The OWASP Top 10 for LLM Applications (2025) describes this as LLM06:2025 Excessive Agency, with three root causes: excessive functionality (the system exposes tools it does not need), excessive permissions (the tool can touch more than its task requires) and excessive autonomy (high-impact actions happen without independent approval). Among its prevention measures: avoid open-ended tools in favour of narrow, purpose-built ones, grant downstream access only at the level needed, and enforce authorization in the downstream system rather than relying on the model's judgment ("complete mediation").
The same page notes that logging, monitoring and rate limiting do not prevent excessive agency; they only limit the damage. The controls below are of the preventing kind.
Two cases, two sets of controls
The OWASP SSRF Prevention Cheat Sheet separates two situations, and the right control set depends on which one applies.
Case 1: the tool only needs a few known destinations. The cheat sheet's guidance is to match the host against an allow-list and build the request in code, instead of passing the model's string to an HTTP client. Most "look up this order", "read this ticket" or "call this internal API" tools belong here, and for them the best design is not a URL parameter at all but an identifier that the server turns into a request.
Case 2: the tool must be able to fetch arbitrary public pages. The cheat sheet states that allow-lists cannot be used here because the list of destinations is not known in advance. Control then moves to the network and to what is checked at connection time. It also warns that deny-lists are bypass-prone and a last resort, and that regular expressions over complex formats are hard to maintain and error-prone.
Five controls for Case 2
- Check the address the socket will use, not the string that was submitted. The cheat sheet notes that domain allow-listing alone does not prevent DNS rebinding, and that checking DNS answers separately does not either. The check has to happen on the resolved address at connection time.
- Do not follow redirects automatically. The cheat sheet recommends disabling redirect-following in the web client. If redirects are needed, each hop is a new request that goes through every check again.
- Block non-routable ranges at the network layer too. Application checks can have gaps (one is described below). Egress rules for the process that runs the tool are the second layer.
-
Make the cloud metadata service hard to reach. On AWS, IMDSv2 uses session tokens obtained with a
PUTrequest; aPUTcarrying anX-Forwarded-Forheader is rejected, and the response hop limit defaults to 1. The instance can be configured to accept only IMDSv2, which makes IMDSv1 calls fail. The OWASP cheat sheet's advice is to migrate to IMDSv2 and disable IMDSv1. AWS describes the design goal in the title of its security blog post: "Add defense in depth against open firewalls, reverse proxies, and SSRF vulnerabilities with enhancements to the EC2 Instance Metadata Service". - Give the tool the smallest possible identity. Run it as a separate process with no cloud role, no internal network route and no credentials in its environment, so that a successful request has nothing valuable to return.
A guard in Node.js, and the gap found while testing it
The following uses only built-in modules. The check lives in a custom lookup function, so it runs on the address the connection will use.
import dns from 'node:dns';
import https from 'node:https';
import net from 'node:net';
// Address ranges a public fetch tool must never reach.
const blocked = new net.BlockList();
for (const [addr, bits] of [['0.0.0.0', 8], ['10.0.0.0', 8], ['100.64.0.0', 10], ['127.0.0.0', 8],
['169.254.0.0', 16], ['172.16.0.0', 12], ['192.168.0.0', 16]]) blocked.addSubnet(addr, bits, 'ipv4');
for (const [addr, bits] of [['::1', 128], ['fc00::', 7], ['fe80::', 10], ['::', 128]]) blocked.addSubnet(addr, bits, 'ipv6');
function guardedLookup(hostname, options, callback) {
dns.lookup(hostname, { ...options, all: true }, (err, addresses) => {
if (err) return callback(err);
const bad = addresses.find(a => blocked.check(a.address, a.family === 6 ? 'ipv6' : 'ipv4'));
if (bad) return callback(new Error(`blocked address ${bad.address}`));
if (options.all) return callback(null, addresses);
callback(null, addresses[0].address, addresses[0].family);
});
}
export function fetchPublic(rawUrl, allowedHosts) {
const url = new URL(rawUrl);
if (url.protocol !== 'https:') throw new Error('https only');
if (!allowedHosts.includes(url.hostname)) throw new Error('host not on the allow-list');
// Node does not call `lookup` for an IP literal, so the guard above would not run for one.
if (net.isIP(url.hostname.replace(/^\[|\]$/g, ''))) throw new Error('IP literals are not accepted');
return new Promise((resolve, reject) => {
const req = https.get(url, { lookup: guardedLookup, timeout: 5000 }, res => {
if (res.statusCode >= 300 && res.statusCode < 400) { res.resume(); return reject(new Error('redirect not followed')); }
const chunks = []; let size = 0;
res.on('data', c => { size += c.length; if (size > 1_000_000) req.destroy(new Error('response too large')); else chunks.push(c); });
res.on('end', () => resolve({ status: res.statusCode, body: Buffer.concat(chunks).toString('utf8') }));
});
req.on('timeout', () => req.destroy(new Error('timeout')));
req.on('error', reject);
});
}
The line about IP literals is there because of a gap in the first version of this example. In the first test run, a request to an address given as a literal IP reached a local test server, while the same request by hostname was rejected: the custom lookup is never called when the host is already an address. After the fix, every notation that the URL parser normalises to an IP (127.1, 0x7f000001, 2130706433, [::1]) is rejected by the same line. The practical lesson: a guard has to be tested with the inputs it is meant to stop, not only with the input it was designed around.
Two limits are worth stating. A response size cap and a timeout are included because an open fetch tool is also a way to make the server spend resources. And the example does not make the open-fetch case safe on its own; the other four controls, and the model not being the only thing standing between a page and an internal service, still apply.
What was tested
The code above was run against a local HTTP server and a stubbed client before publication:
- the address ranges: loopback, private, link-local, carrier-grade NAT and unique-local ranges were blocked, and two public addresses were not
- a hostname that resolves to loopback (
localhost) was rejected at connection time - IP literals in several notations were rejected
- a non-HTTPS URL and a host outside the allow-list were rejected
- a 3xx response was refused (tested with a stubbed client, so the redirect rule is checked in isolation)
Not tested: a real DNS-rebinding setup, an IPv6-only network, and behaviour behind a corporate proxy. Those need to be checked in the environment where the tool runs.
A test list for your own agent
Each row is a request an authorised tester makes through the agent's own fetch tool, in an environment they own, with the expected result.
| The tool is asked to fetch | Expected |
|---|---|
| A normal public page on an allowed host | Content returned |
| A loopback address | Rejected |
| A private-network address | Rejected |
| The cloud metadata address | Rejected |
| A hostname that resolves to a private address | Rejected |
| An address in a non-standard notation | Rejected |
| A public URL that answers with a redirect | Redirect not followed |
A plain http:// URL |
Rejected |
| A very large response | Truncated or rejected |
If any row returns content, the tool can reach more than its description says, whatever the system prompt says.
Practise the review
Breachloom has a chapter that frames the problem from the agent's side, A fetch tool lends the assistant the server's network position, a hands-on exercise, Agent metadata fetch, and a general SSRF checklist, SSRF Explained. For the authorization side of agent tools, there is AI Agent Security: A Safer Prompt Cannot Replace Tool Authorization. The chapters and the free IDOR case need no account; the full set of challenges is part of a paid plan.
Which tool in your own agent takes a URL, and what network can its process reach today? Answering that from the deployment, not from the code, is usually the quickest way to find out.
Sources
- OWASP Top 10 for LLM Applications 2025, LLM06:2025 Excessive Agency. https://genai.owasp.org/llmrisk/llm062025-excessive-agency/
- OWASP Server-Side Request Forgery Prevention Cheat Sheet (sections: Cases, Application layer, Case 2, IMDSv2 in AWS, Deny-list). https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html
- AWS documentation, "Use the Instance Metadata Service to access instance metadata" (IMDSv2 session tokens,
PUTwithX-Forwarded-Forrejected, default hop limit of 1, IMDSv2-only option). https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html - AWS Security Blog, "Add defense in depth against open firewalls, reverse proxies, and SSRF vulnerabilities with enhancements to the EC2 Instance Metadata Service". https://aws.amazon.com/blogs/security/defense-in-depth-open-firewalls-reverse-proxies-ssrf-vulnerabilities-ec2-instance-metadata-service/
- Node.js documentation:
net.BlockList,dns.lookup, and thelookupoption ofhttps.request.
來源:DEV Community · dev.to