# AI agent file uploads need hard boundaries

An upload endpoint is an outbound data channel, even when engineers call it an attachment feature. Once an AI agent can attach a log, export, screenshot, or customer file, a vague instruction such as "send this to support" can turn into an irreversible disclosure.

AI agent file uploads need hard boundaries around bytes, content, and receivers. Put those boundaries in the action that performs the upload, not in the agent prompt and not in a reviewer’s ability to spot a bad filename at the end of a busy day.

## An upload action must declare what may leave

A safe upload flow starts by treating every file as an object with a declared purpose. The purpose determines its maximum size, accepted formats, permitted receiver, retention expectation, and whether a person must approve the send. If your API accepts an arbitrary multipart body and a caller supplied URL, you have created a general data exfiltration route with a pleasant developer interface.

The distinction people blur is between receiving files from an untrusted client and sending files on behalf of an agent. Traditional upload guidance focuses on protecting your server from malicious files. Agent uploads need that protection too, but the more immediate risk is protecting data from an overbroad send. A PDF may be perfectly safe to parse and completely inappropriate to deliver to a ticketing system.

Define named upload purposes instead of a generic `upload_file` action. A purpose such as `diagnostic_bundle` can permit a compressed support bundle to one support receiver. A purpose such as `invoice_export` can permit a CSV to the accounting receiver. Neither purpose should accept a path, receiver URL, and arbitrary file in the same request.

A small contract makes the boundary visible:

```json
{
  "purpose": "diagnostic_bundle",
  "file_path": "/private/tmp/app-diagnostics-2025-03-08.zip",
  "destination_id": "support-case",
  "case_reference": "CASE-1842"
}
```

The caller chooses an approved purpose and supplies metadata needed for the business action. The upload service maps `support-case` to a receiver it owns. It does not let the caller replace that receiver with a URL embedded in an issue comment, a document, or a tool response.

Keep file paths under control as well. An agent should only select files from designated staging directories, or the service should build the attachment itself from known inputs. Letting an agent name any readable path turns a request to attach diagnostics into a request to read configuration files, SSH material, browser data, or another user’s export.

## Size limits need two measurements

A file limit should reject an excessive body before your service stores, scans, or forwards it. Enforce it at the HTTP edge with `Content-Length` when present, then count bytes while reading because a client can omit or lie about that header.

The limit must fit the purpose. A 25 MB limit for a customer CSV and a 25 MB limit for a compressed diagnostic bundle are not equivalent decisions. The CSV can expand into a much larger memory allocation when a parser reads it. The archive may decompress to many times its transport size. Set both a wire-size limit and a processed-size limit.

Do not let an agent split a rejected file into many legal requests unless the receiver explicitly supports chunked uploads and your service tracks the total. Otherwise, a 10 MB rule becomes a 100-part transfer with none of the protection you thought you had.

Reject early and return a response that lets the agent recover safely:

```json
{
  "error": "attachment_too_large",
  "purpose": "diagnostic_bundle",
  "observed_bytes": 12582911,
  "max_bytes": 8388608,
  "safe_alternatives": [
    "create_redacted_diagnostic_bundle",
    "attach_selected_log_window"
  ]
}
```

That response matters. If rejection only says "upload failed," an agent may try a different destination, compress the file, or keep retrying. Tell it what action is allowed next, while refusing to expose the rejected file through a debug message.

Buffering is another quiet failure. Many frameworks parse a multipart request into memory or a temporary directory before application code sees the size. Configure the web server, framework parser, reverse proxy, and application reader to agree on a limit. The smallest limit wins, but an unexpectedly larger layer can still consume disk space before the smaller layer rejects the request.

Measure post-processing size for text normalization, image conversion, document extraction, and archive unpacking. A limit that only covers the original attachment does not control the resource use or disclosure potential of the material you later generate.

## A filename and MIME header prove almost nothing

File type checks need independent evidence because a filename extension and `Content-Type` header come from the sender. An agent may relay a misleading label without malicious intent. A support tool may call every attachment `application/octet-stream`. Either way, the receiver needs to decide from bytes and permitted structure.

OWASP’s File Upload Cheat Sheet recommends allow-listing extensions, not trusting the `Content-Type` header, generating server-side filenames, and storing uploads outside the webroot. That guidance holds up, but agent workflows need one extra rule: validate the type against the named purpose before the service contacts the remote receiver. A valid PDF is not automatically valid for every upload purpose.

Use several checks that answer different questions:

- The extension tells you what the sender claims to have.
- Signature bytes tell you whether the content begins like the claimed format.
- A bounded parser tells you whether the bytes meet enough of the format to handle them safely.
- Content inspection tells you whether the file contains prohibited material for that purpose.

For a CSV export, accept a small allow list such as `.csv` and UTF-8 text, parse a limited sample, and reject embedded binary data or unexpectedly wide rows. For a PDF, verify the `%PDF-` signature, apply a size limit, and use a parser with time and memory limits if you must inspect pages. For images, decode dimensions before processing; an image with modest file size can still allocate far too much memory when decoded.

Avoid a blanket "archive" type. ZIP, TAR, and GZIP differ, and each creates its own inspection work. If a business process does not need an archive, refuse it. Accepting a format because users sometimes want it is how generic attachment endpoints get built.

Rename accepted files on the service side. Preserve the original filename as display metadata after you sanitize it, but do not use it as a filesystem path, object-store key, or `Content-Disposition` value without escaping. Names can contain control characters, deceptive Unicode, path separators, and strings that alter downstream logs.

## Destination checks must survive redirects

A receiver allow list must identify the exact place that may receive the file. Allowing `https://example.com` is incomplete if the HTTP client follows redirects to another host, resolves an internal address, or accepts a different port.

Store destinations as server-side records with a fixed scheme, host, port, path prefix, credential identity, and accepted purposes. The action receives `destination_id`, never a freeform endpoint. If a receiver requires a case ID in the path, construct it from a constrained identifier rather than taking a full URL from the agent.

For every send, the HTTP client should enforce these checks:

1. Require HTTPS unless you have a documented internal exception.
2. Match the requested host and port against the destination record before connecting.
3. Disable redirects by default. If a receiver needs redirects, validate every redirect target against the same record before sending another byte.
4. Reject IP literals, loopback addresses, link-local addresses, and private ranges unless that destination explicitly exists for a controlled internal service.
5. Pin the allowed path prefix and HTTP method instead of allowing an entire host.

The fourth point is often described as SSRF protection, and it is that. It also prevents accidental disclosure through an agent that follows a URL from a task description. An upload client with access to internal networks should never treat task text as authority to contact an address.

Do not forward the original authorization context. The credential used to send to a case-management API should belong only to that receiver and action. A file upload gateway that copies arbitrary agent headers creates an easier path for header injection and gives an agent a way to select credentials indirectly.

Record the final URL after redirects, but redact query values from operational logs. Query strings frequently carry signed upload tokens. Logging the destination is useful; reproducing a usable authorization token is careless.

## Logs need a deliberate export path

Logs are the attachment class that teams underestimate most. They capture request headers, customer identifiers, SQL fragments, stack traces, internal hostnames, and sometimes complete request or response bodies. The fact that a log sits in a developer directory does not make it safe to email or upload.

Do not solve this with an instruction that tells the agent to "remove secrets." Agents can miss formats, redact too much, or decide a suspicious string is harmless context. Create a diagnostic-bundle builder that reads known files, applies deterministic filters, and emits a new artifact into a staging directory.

A practical filter policy can remove entire fields rather than hunting every possible secret pattern. Remove `Authorization`, `Cookie`, `Set-Cookie`, API key fields, session identifiers, and request bodies by default. Replace customer identifiers with stable local tokens when correlation matters. Limit timestamps to the incident window instead of shipping weeks of history.

For example, this is a safer shape for a diagnostic record than a raw HTTP trace:

```json
{
  "time": "2025-03-08T14:22:11Z",
  "request_id": "local-7f3c",
  "method": "POST",
  "route": "/v1/reports",
  "status": 502,
  "upstream": "reporting-service",
  "authorization": "[removed]",
  "body": "[omitted]"
}
```

The bundle builder should produce a manifest with file names, byte counts, hashes, and applied filters. That gives a reviewer something concrete to inspect without opening every attachment. It also lets the receiver identify a truncated or modified upload.

Database exports need a stricter rule. An agent should not attach a production export merely because a ticket says "send a sample." Generate a schema-only artifact, a purpose-built query with approved columns, or a synthetic reproduction. If an incident requires actual customer records, make that a separate action with explicit destination, scope, and human approval.

Screenshots deserve the same suspicion. They can include customer accounts, messages, browser tabs, notifications, and local paths. Crop or generate a focused capture through a controlled tool rather than handing an agent a broad screenshot directory.

## Archives and office documents hide more than one file

An archive creates a second upload set inside the first. Before you send one, inspect its member list and enforce limits on member count, compressed bytes, expanded bytes, path depth, and nesting. Reject entries that use absolute paths, `..` traversal, duplicate names, or symlinks.

A ZIP file that is 2 MB on disk can expand to a size that exhausts a worker or a receiver. This is usually called a decompression bomb, but the failure is not limited to malicious inputs. Build systems can produce enormous archives by mistake, and an agent may attach the first file that resembles an export.

Office documents need similar care. Modern document formats often contain ZIP containers, embedded media, metadata, comments, tracked changes, and external relationships. A document can look redacted on the page while retaining prior text or author information in its package. If the workflow only needs rendered content, produce a fresh PDF from approved data instead of forwarding the editable original.

Do not recursively "sanitize" arbitrary documents in the request path. Parsing and rewriting complex formats has its own security and reliability cost. For a narrow workflow, accept a narrow set of generated artifacts. For an exceptional document, route it to a human review process that can inspect the actual file and its destination.

## The action contract should make unsafe requests impossible

An agent-facing tool should expose choices that match your controls, not a raw HTTP request builder. If the tool offers `url`, `headers`, `file_path`, and `method`, the policy has already lost most of its shape.

Use a request schema where untrusted fields describe intent and trusted records supply authority. This example keeps the recipient, credentials, and permitted file class outside the agent’s control:

```json
{
  "action": "send_attachment",
  "purpose": "customer_export",
  "destination_id": "finance-import",
  "artifact_id": "exp_8c4e1a",
  "note": "March reconciliation correction"
}
```

The service resolves `artifact_id` to a staged object that it created or accepted through a separate intake flow. It calculates a digest and detected type itself. It resolves `destination_id` to a fixed receiver record. It adds the recipient credential at send time. The agent never sees that credential and cannot substitute a second recipient after approval.

The preflight result should expose enough detail for an informed decision:

```json
{
  "decision": "approval_required",
  "artifact": {
    "name": "reconciliation-2025-03.csv",
    "bytes": 482913,
    "detected_type": "text/csv",
    "sha256": "a4d1...c09e"
  },
  "destination": {
    "label": "Finance import",
    "host": "imports.example.internal",
    "path": "/v2/reconciliation"
  },
  "reason": "customer_export requires approval"
}
```

Do not show a reviewer only the filename and an approve button. Show the measured size, detected type, destination host, destination label, and purpose. If the file is sensitive, show a sampled classification result or manifest, not its full contents in the approval surface.

Make artifact IDs short-lived and single-purpose. A staged diagnostic bundle should not remain reusable as a generic upload object after its support case closes. Bind it to the purpose and receiver when you create it, then expire it after a short operational window.

Retries need idempotency. Network failures are common, and agents retry aggressively. Generate an idempotency token at the action layer, bind it to the artifact digest and destination, and reuse it only for the same intended send. Do not let a retry with a changed path or changed digest inherit the earlier authorization.

## Human approval works when it marks a real boundary

Approval does not replace validation. A person cannot reliably identify a ZIP bomb, a disguised file, or a redirect flaw from a modal. Approval is for the decision that policy cannot make automatically: whether this particular customer export should go to this recipient for this incident.

Use per-call approval for high-sensitivity sends, new recipients, production exports, and broad diagnostic artifacts. Let routine, narrow artifacts move under an approved purpose if the destination and contents are constrained. Asking a person to approve every test report teaches them to click through without reading.

The approval record should bind to the artifact digest, declared purpose, and destination record. If any of those change, discard the approval. A filename is not enough because two files can share a filename while carrying different bytes.

Sallyport can keep the agent away from the credential used for an HTTP upload, and its per-key approval option fits sends that deserve a person’s decision every time. That does not remove the need for an application-level artifact contract, because the gateway cannot infer whether a customer CSV belongs in a support case.

Keep two audit views: one that tells you which agent run received authority to act, and another that records each transfer attempt and result. An audit record should include digest, measured bytes, type verdict, purpose, recipient record, decision, response status, and time. Store secrets and full file contents elsewhere, if you retain them at all.

## Test the rejection paths before an agent finds them

A policy that only succeeds in the happy path is unfinished. Build an isolated receiver that records the exact request it received, then use fixtures that force each boundary to fire.

Start with a plain upload request against your test receiver:

```bash
curl -i -X POST https://receiver.test/attachments \
  -H 'Authorization: Bearer test-token' \
  -F 'file=@fixtures/diagnostic.zip;type=application/zip' \
  -F 'case_reference=CASE-1842'
```

A successful test should confirm the method, final host, path, digest, and size. The useful tests are the ones that prove the receiver got nothing. Assert that an over-limit body is rejected before forwarding, a forged `text/plain` header cannot pass a binary payload, and an unlisted destination never receives a connection.

Include these cases in your test fixture set:

- A valid extension with mismatched signature bytes.
- A compressed archive whose expanded size exceeds the rule.
- A redirect from an allowed host to an unallowed host.
- A log fixture containing an authorization header and a customer email address.
- A retry that uses the same idempotency token with different file bytes.

Inspect server logs during these tests. You are checking two things: that the transfer did not happen, and that your own logs did not preserve the rejected body, bearer token, or signed URL. Teams often fix the network path while leaving the same sensitive material in exception traces.

Run the tests through the same agent tool interface that production agents use. A secure internal HTTP client does not help if the agent-facing wrapper can select a raw URL or bypass the artifact staging path.

## Make the safe route easier than the raw route

Teams bypass upload controls when the approved path is slow, ambiguous, or cannot handle ordinary support work. Build a small set of artifacts people actually need: a redacted diagnostic bundle, a narrow export, a generated report, and an incident screenshot with defined bounds. Make each one easy for an agent to request by name.

Keep the generic raw uploader out of the agent tool list. If an engineer needs it for an unusual case, require an interactive operational path where they choose the file and receiver with full context. That inconvenience is appropriate because the action has no reliable automated classification.

Review rejected sends as seriously as successful ones. A pattern of oversized bundles means your diagnostic artifact is wrong. Repeated attempts to send logs to a new host may mean the destination list needs a justified addition, or it may mean an agent prompt is trying to route around your controls. The difference appears in the record only if you capture purpose, file verdict, and receiver decision together.

The first change I would make is simple: remove freeform URLs and arbitrary file paths from the agent upload action. Once the action accepts only staged artifacts, named purposes, and destination IDs, size limits and content checks have somewhere dependable to operate.
