# 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 1. **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. 2. **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 (Anthropic `web_fetch`, OpenAI `web_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 ` 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-Type` header 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: attachment` and 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 1. Allow only `http`/`https`; deny `file:`, `gopher:`, `ftp:`, `data:`. 2. 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. 3. Follow redirects manually (max 3) and re-validate every hop. 4. Strip credentials from URLs; never forward your own cookies/authorization; use a dedicated egress identity. 5. Cap response size and time; stream to disk with a limit; only accept expected content types. 6. Prefer an allowlist of domains (`domain-allowlists.md`); Anthropic's `web_fetch` gets this for free via `allowed_domains` plus the URL-in-context rule — reuse the same list for your own fetcher. 7. 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_after` on 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 `fileData` read 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`/`urlContext` URLs validated before you pass them to any provider. - [ ] Gemini resumable upload URLs (`x-goog-upload-url`, contain `upload_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.