JJoeven

Curriculum/Tools & Function Calling

Path and Host Allowlists

read_file needs a folder jail. http_get needs a host allowlist. Check before you fetch, not after.

advanced21 min22 / 24

Even a tool on the allowlist can be abused by arguments.

  • read_file(path) must stay under a workspace root. Reject ../.env and absolute paths you did not plan.
  • http_get(url) must check the host before any request. Fetching first and filtering later still sent the packet (and maybe your cloud metadata).

Never let the model pass a raw SQL string. If you need SQL, expose get_order(order_id) instead. Extra keys already blocked the SQL field. Path and host checks block the remaining smuggle: a legal field whose value is the attack.

These checks belong in the tool implementation and in a shared policy helper. Prompts that say “stay in /workspace” are not gates. The worker is the gate. Check before IO.

Check, then IO
ArgsJail checkOpen or fetch

Filter before you touch the world. Fetch then filter still sent the packet.

Check, then IO

Paths

Canonicalize, then prefix-check. Naive startswith(ROOT) loses to /workspace/../.env unless you also reject .. and extra slashes, or you resolve to an absolute real path inside a jail and compare. Classroom code rejects any .. segment and requires the root prefix. Production should resolve and then compare to the real workspace root, on a machine where that resolve cannot follow a symlink out of the jail if policy forbids it.

Absolute paths the model invented (/etc/passwd) fail the prefix. Relative paths should be joined to the root in the handler, not passed through from the model as “maybe relative, maybe not.” Pick one: always relative to workspace, always absolute under workspace. Document it in the schema description. Still enforce in code.

read_file is a read. write_file is a write with a tighter jail and usually approval. Do not share the path helper’s defaults accidentally with a delete tool.

Hosts

Parse the URL, extract host, compare to an allowlist. Block link-local and localhost unless the tool is explicitly a loopback debug probe. Cloud metadata IPs (169.254.169.254 and cousins) are a classic exfil. Redirects: if you follow them, re-check the host after each hop, or do not follow. Fetching then filtering still talked to the attacker on the first hop.

DNS rebinding and weird encodings exist. Use a real URL parser, not a split on slashes, in production. The classroom parser is naive on purpose so you can see the when: before PAGES lookup, before any socket. The order is the lesson. The parser quality is your platform team’s job.

HTTPS does not make a host safe. evil.example can be HTTPS. Allowlist names, not schemes alone.

Check, then IO

The observation should be path_denied or blocked_host without a body from the forbidden place. If you include the file contents “for the error,” you lost. If you include the metadata document “to debug,” you lost.

The same helper should run in MCP file servers and in local tools. Hosts do not get a pass because the URL came from a resource.

Classroom jail

Root is /workspace/. Allow host is example.com. Good file, escape, passwd, good http, evil host, metadata IP. Only the two “ok” lines succeed. The metadata call never “fetches.” There is no network here; the allowlist still runs first.

Live PythonOpen full playgroundpython
Output
Run to execute this in your browser. Nothing is sent to a server.

What printed: README ok, .. denied, passwd denied, example.com hello, evil blocked, metadata blocked. The .. path never opens. Evil hosts never fetch. Check before IO.

What goes wrong

Filter after fetch. startswith without canonicalization. Allowing *. Following redirects off-list. Passing SQL. Logging denied paths that include secrets from query strings. Using the prompt as the only jail. These send packets or open files you did not intend.

How to test allowlists

A table of paths to ok/denied including .., encoded dots if you handle them, absolute escapes, the exact root. A table of URLs including metadata IPs, localhost, evil hosts, the allowed host, a redirect target if you implement hops. Assert no handler IO on deny — flags again.

Canonicalize, then compare, then touch the world

Path jails fail when you prefix-check a string the model still controls. Resolve to a real path inside the workspace, then confirm it still sits under the real root. Reject .. segments, odd encodings, and symlink walks if policy forbids leaving the jail. Pick one public shape in the schema — always relative to workspace, or always absolute under it — and join in the handler. Do not accept both. write_file and delete tools need the same helper with tighter risk labels, not a copy that forgot the check.

Hosts fail when you fetch first. Parse, allowlist, then connect. Re-check after every redirect hop, or do not follow redirects. HTTPS does not make evil.example safe. Block link-local, localhost, and cloud metadata unless the tool is an explicit loopback probe. Production parsers are real URL libraries. Classroom split-on-slash is to show when. DNS rebinding is why you do not pin an IP once and forget the name.

SQL never belongs as an argument. get_order(order_id) writes the query. Denied paths and hosts must not include the forbidden body “for debugging.” The observation is path_denied or blocked_host. MCP file servers use the same helpers. Prompts that say “stay in workspace” are not a jail. Tests should include the ugly strings: encoded dots, extra slashes, a host that looks like example.com.evil.example, and a metadata IP on HTTP. If those are not in CI, they will be in an incident. The helper that returns None must be the only path to open() or to the HTTP client.

How agents use this

Put the same allowlists in the worker, even if the prompt already lists them. Prompts are not gates. For cloud metadata IPs, block link-local and localhost unless the tool is explicitly a loopback debug probe. The loop should see path_denied and stop guessing paths, not retry ../...

Schema can still say “path relative to workspace.” The worker still checks. Dual gate.

Watch out:Filter then fetch. Fetch then filter still talks to the attacker.

Check your understanding

When should you check that a URL host is allowed?