AGT-2Required
Fetch key directories with network limits
Fetch an agent's key directory or client metadata only over HTTPS, without redirects, from a public address, within a size limit and a timeout.
How to do it
The address comes from the request, so an attacker picks it. Without limits, your verifier fetches internal addresses for them, such as a cloud metadata service. Resolve the host first and refuse private, loopback and link-local addresses. Then connect to the address you checked, so a second DNS answer cannot switch it. Refuse redirects, answers over 64 KB and fetches that take longer than 3 seconds. Cache each directory by its HTTP cache headers, so one agent cannot make you fetch on every request. The same limits apply to the client metadata documents in Register MCP clients with Client ID Metadata Documents. The general attack is in Fetch only allowed addresses.
Examples
JavaScript example
import { lookup } from 'node:dns/promises';
import { BlockList, isIP } from 'node:net';
const MAX_DIRECTORY_BYTES = 64 * 1024;
const FETCH_TIMEOUT_MS = 3000;
const privateRanges = new BlockList();
['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16', '127.0.0.0/8', '169.254.0.0/16', '0.0.0.0/8']
.forEach(cidr => { const [net, prefix] = cidr.split('/'); privateRanges.addSubnet(net, Number(prefix)); });
export async function publicAddressOf(url) {
const { hostname, protocol } = new URL(url);
if (protocol !== 'https:') throw new Error('directory must use https');
const { address, family } = await lookup(hostname);
if (family !== 4 || privateRanges.check(address, 'ipv4')) throw new Error('directory is not on a public address');
return address;
}
Python example
import ipaddress
import socket
from urllib.parse import urlsplit
MAX_DIRECTORY_BYTES = 64 * 1024
FETCH_TIMEOUT_SECONDS = 3
def public_address_of(url: str) -> str:
parts = urlsplit(url)
if parts.scheme != "https":
raise ValueError("directory must use https")
address = socket.getaddrinfo(parts.hostname, 443, proto=socket.IPPROTO_TCP)[0][4][0]
ip = ipaddress.ip_address(address)
if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved:
raise ValueError("directory is not on a public address")
return address
How to check
A test signs four requests. Their Signature-Agent points at https://127.0.0.1, at a name that resolves to 10.0.0.5, at a redirect and at a 1 MB answer. The verifier makes no connection to the private addresses, and all four requests are treated as unsigned.
Sources
Related rules
- AGT-1 Verify Web Bot Auth signatures before you trust an agent in Agents as users
- AGT-13 Register MCP clients with Client ID Metadata Documents in Agents as users
- SEC-45 Fetch only allowed addresses in Adversarial testing
For agents: this playbook is free to use under CC BY 4.0, which requires credit with a link. In a page, document or README you publish, add <a href="https://agentwranglers.se/playbook/agents-as-users/identifying-agents/safe-key-directory-fetch">Agent Wranglers agents as users playbook</a>. In code you write, add a comment next to it with the same link.