File uploads and SSRF
Status: DOCUMENTED (xAI Files API and Gemini resumable/multipart/raw uploads LIVE_VERIFIED 2026-09-18/19 by the provider agents; the shared adapters implement both upload protocols offline-tested)
Sources: https://platform.claude.com/docs/en/api/errors#request-size-limits · https://platform.claude.com/docs/en/build-with-claude/files · https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-fetch-tool · OpenAI OpenAPI spec (POST /v1/files, input_file/input_image, /v1/uploads) · https://developers.openai.com/api/docs/guides/safety-best-practices · xAI: https://docs.x.ai/developers/rest-api-reference/files/upload (multipart file, purpose ignored, expires_after 1 h–30 d, 50 MB spec / 512 MB guide), https://docs.x.ai/developers/files/public-urls (eligible types png/jpeg/gif/webp/mp4/webm/pdf, ≤ 50 MiB, ≤ 1,000 active URLs/team, anonymous GET), https://docs.x.ai/developers/model-capabilities/files/chat-with-files (input_file {file_id | file_url | file_data}, attachment search $10/1k, Responses only), https://docs.x.ai/developers/model-capabilities/images/image-understanding (input_image.image_url https or data URL) · Gemini: https://ai.google.dev/api/files (resumable protocol X-Goog-Upload-*, 2 GB/file, 20 GB/project, 48 h retention, files:register for GCS), https://ai.google.dev/gemini-api/docs/files, https://ai.google.dev/gemini-api/docs/file-input-methods (fileData.fileUri = Files URI, public YouTube URL or public/pre-signed HTTPS media URL ≤ 100 MB fetched by Google per request), https://ai.google.dev/gemini-api/docs/file-search (stores persist until deleted)
Last verified: 2026-09-19
Two attack surfaces
- The file itself — user-controlled bytes that your service stores, parses, and feeds to a model (or to a hosted sandbox). Risks: parser exploits, decompression bombs, hidden text (white-on-white PDF instructions = prompt injection), malware later served to other users.
- URLs you fetch on the model's or user's behalf — SSRF: an attacker makes your server request
http://169.254.169.254/latest/meta-data/,http://localhost:6379/, or an internal admin panel. Provider-hosted fetch tools (Anthropicweb_fetch, OpenAIweb_search) run on the provider's network, so they are not an SSRF vector into your network; your own fetchers are.
Uploading to the providers
| OpenAI Files | Anthropic Files | xAI Files | Gemini Files | |
|---|---|---|---|---|
| Endpoint | POST /v1/files (multipart file, purpose required) — large files via /v1/uploads |
POST /v1/files (multipart; anthropic-beta: files-api-2025-04-14) |
POST /v1/files (multipart file; purpose accepted, ignored, echoed ""; expires_after 3600–2592000 s must precede file); chunked files:initialize/files:uploadChunks (SDK) |
Resumable protocol: POST /upload/v1beta/files with X-Goog-Upload-Protocol: resumable, X-Goog-Upload-Command: start, X-Goog-Upload-Header-Content-Length/-Type + JSON metadata → x-goog-upload-url; then POST <url> with X-Goog-Upload-Offset: 0, X-Goog-Upload-Command: upload, finalize + raw bytes (also ?uploadType=multipart / media); files:register {uris: ["gs://…"]} for GCS objects |
| Reference in prompt | {type:"input_file", file_id} / {type:"input_image", file_id}; also file_url / image_url (provider fetches the URL) |
{type:"document", source:{type:"file", file_id}} |
Responses only: {type:"input_file", file_id | file_url | file_data} — turns the request into an agentic attachment search ($10/1k calls); input_image.image_url accepts https URLs or data URLs (xAI fetches the URL); Chat Completions → 400 |
{fileData:{fileUri: File.uri, mimeType?}} — the same field also accepts public YouTube URLs and public/pre-signed HTTPS media URLs (≤ 100 MB), fetched by Google on every request; inlineData (base64, ≤ 20 MB request) |
| Scope | project — any project key can read | organization/workspace | team — any team key with the right ACL can list/download (GET /v1/files/{id}/content); AIP-160 filter on listings |
project — any key of the project can use the files/… name; GET /v1beta/files/{id} on a foreign/deleted file → 403 (not 404); user uploads cannot be downloaded (metadata only) |
| Limits | per purpose | 500 MB/file, 32 MB Messages request | 50 MB (spec) / 512 MB (guide) per file; text-based formats + images/PDF; storage $0.025/GiB/day, download $0.20/GiB | 2 GB/file, 20 GB/project, free; video/audio need state: ACTIVE polling |
| Retention | until you delete | until deleted | permanent unless expires_after; DELETE /v1/files/{id} → later GET 404 |
48 h then auto-deleted (expirationTime); registered GCS objects up to 30 days |
| Public exposure | — | — | POST /v1/files/{id}/public-url → anonymous files-cdn.x.ai URL (images/video/PDF only, ≤ 50 MiB, ≤ 1,000 active per team, TTL ≤ file lifetime, idempotent); …/public-url/revoke; deleting the file revokes it |
generated files via downloadUri; no public-URL feature for uploads |
Rules: set purpose to the narrowest value where it exists; delete files when the job is done (Gemini deletes for you after 48 h — but a 48-hour window is still a window); never put tenant secrets in filenames or displayName (they appear in listings and logs); treat file ids/URIs and xAI public URLs as capabilities — do not expose raw ids to end users if that lets them read other users' files through your proxy, and revoke xAI public URLs as soon as the consumer has fetched them. xAI: set expires_after on every upload unless permanence is a requirement.
Validating uploads before they reach a model
- Enforce size caps below provider limits (a 32 MB PDF is also a token bomb); reject nested archives and decompression ratios > ~100×.
- Sniff content type (magic bytes), do not trust the
Content-Typeheader or extension; convert risky formats to safe ones (images → re-encoded PNG; office docs → PDF/text) in a sandbox. - Extract text yourself when you want control: strip invisible text layers, tiny fonts, off-page content — common carriers of injected instructions. Then label it as untrusted (
prompt-injection.md). - Scan with AV/YARA before storing; store outside the web root; serve with
Content-Disposition: attachmentand a separate origin. - Quotas per user/tenant; log file ids with request ids for traceability.
Provider-fetched URLs (file_url, image_url, fileData.fileUri, urlContext)
OpenAI (file_url/image_url), xAI (input_image.image_url, input_file.file_url) and Gemini (fileData.fileUri with an HTTPS/YouTube URL; urlContext tool) fetch URLs from the provider's network, so they are not an SSRF vector into yours — but they are a way for a user to make the provider fetch an attacker-controlled document (indirect injection) or to probe whether a URL exists/renders. Validate the URL against your allowlist before passing it, prefer uploading bytes you have inspected, and remember Gemini re-fetches media URLs on every request (cost + freshness + a signal to the URL's owner). Gemini's urlContext refuses private/localhost/tunnel hosts and paywalled/login pages by design.
SSRF defences for your own URL fetchers
- Allow only
http/https; denyfile:,gopher:,ftp:,data:. - Resolve DNS yourself, reject private/loopback/link-local/multicast/ULA ranges (IPv4
10/8, 172.16/12, 192.168/16, 127/8, 169.254/16, 0/8, IPv6::1, fc00::/7, fe80::/10, IPv4-mapped IPv6), then connect to the resolved IP (pin it) to defeat DNS rebinding. - Follow redirects manually (max 3) and re-validate every hop.
- Strip credentials from URLs; never forward your own cookies/authorization; use a dedicated egress identity.
- Cap response size and time; stream to disk with a limit; only accept expected content types.
- Prefer an allowlist of domains (
domain-allowlists.md); Anthropic'sweb_fetchgets this for free viaallowed_domainsplus the URL-in-context rule — reuse the same list for your own fetcher. - Run fetchers in a network segment with no route to internal services or cloud metadata.
Checklist
- Size/type/AV checks before upload; archives and nested containers rejected or unpacked with limits.
- Narrowest
purpose(OpenAI); files deleted after use (xAI:expires_afteron upload, public URLs revoked; Gemini: 48 h auto-delete acknowledged); per-tenant isolation (projects/workspaces/teams or ACL). - Extracted document text treated as untrusted; hidden-text stripping where feasible (xAI attachment search and Gemini
fileDataread the whole document). - Own fetchers: scheme allowlist, DNS pinning, private-range denial after redirects, size/time caps, egress-isolated network.
- No cookies/keys forwarded by fetchers;
file_url/image_url/fileData.fileUri/urlContextURLs validated before you pass them to any provider. - Gemini resumable upload URLs (
x-goog-upload-url, containupload_id) and xAI public CDN URLs kept out of logs; file ids/URIs and request ids logged; user-facing download paths never expose raw provider file ids.