---
title: "Serve private files through short-lived · Agent Wranglers"
url: https://agentwranglers.se/playbook/files-and-media/serving-files/signed-downloads
id: MED-46
level: Required
risk: data
updated: 2026-10-22
build: a2a6bdfed830
tokens: 1564
licence: "CC BY 4.0"
licence_url: https://creativecommons.org/licenses/by/4.0/
---

# Serve private files through short-lived links

For AI agents: the [site map](https://agentwranglers.se/llms.txt) lists everything here, and this page sits under [Serving files](https://agentwranglers.se/playbook/files-and-media/serving-files.md).

By [Agent Wranglers](https://agentwranglers.se/authors/agent-wranglers.md). Updated 2026-10-22.

- ID: MED-46
- Level: Required
- Chapter: [Serving files](https://agentwranglers.se/playbook/files-and-media/serving-files.md)
- Rule: Keep the bucket private. Check access on every download request, then redirect to a signed address that expires within 5 minutes and sets safe download headers.
- Check: Fetch the bucket address of a private file without a signature, and storage answers 403. A signed address works, then answers 403 after 5 minutes. A file named `rapport "Q3"\r\nX: y.pdf` downloads with one `Content-Disposition` header and no extra header.
- Risk: Data leaked or lost
- How: Block all public access on the bucket. The API checks the caller may see this file, as in [authorise every object](https://agentwranglers.se/playbook/api-design/api-security/object-authorisation.md), then answers `302` with a fresh signed address. Never store signed addresses or put them in emails, since they work for anyone until they expire. Set `Content-Disposition` in the signature. Use `attachment` for anything but images and PDFs you show inline. Give an ASCII `filename` and a UTF-8 `filename*` as RFC 6266 and RFC 8187 describe. Replace quotes, backslashes and line breaks in the name. Always use `attachment` for SVG, HTML and XML, because they can run scripts. Also set the content type and `X-Content-Type-Options: nosniff`. Google Cloud Storage takes `response-content-disposition` in the signed address, and Azure takes `rscd` in the SAS. Serve user files from a separate domain, such as `files.example-usercontent.com`. The bucket itself stays closed, as [Keep storage buckets private](https://agentwranglers.se/playbook/application-security/secrets-and-configuration/private-storage-buckets.md) says.

## Examples

**Using these examples:** they show one way to follow this rule. Keep the behaviour and the check, not the syntax. Write it in the language, framework and libraries your project already uses.

### JavaScript example

```js
import { GetObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';

const DOWNLOAD_LINK_SECONDS = 300;
const asciiFallback = name => name.replace(/[^\x20-\x7e]|["\\]/g, '_');
const rfc8187 = name => encodeURIComponent(name).replace(/['()*]/g, c => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);

export function downloadLink(s3, file) {
  const disposition = `attachment; filename="${asciiFallback(file.name)}"; filename*=UTF-8''${rfc8187(file.name)}`;
  const command = new GetObjectCommand({ Bucket: 'example-shop-files', Key: file.key, ResponseContentDisposition: disposition });
  return getSignedUrl(s3, command, { expiresIn: DOWNLOAD_LINK_SECONDS });
}
```

### Python example

```python
import re
from urllib.parse import quote

DOWNLOAD_LINK_SECONDS = 300

def ascii_fallback(name: str) -> str:
    return re.sub(r'[^\x20-\x7e]|["\\]', "_", name)

def download_link(s3, file) -> str:
    disposition = f"attachment; filename=\"{ascii_fallback(file.name)}\"; filename*=UTF-8''{quote(file.name, safe='')}"
    return s3.generate_presigned_url(
        "get_object",
        Params={"Bucket": "example-shop-files", "Key": file.key, "ResponseContentDisposition": disposition},
        ExpiresIn=DOWNLOAD_LINK_SECONDS,
    )
```

## Sources

- [RFC 6266 Content-Disposition in HTTP](https://www.rfc-editor.org/rfc/rfc6266)
- [RFC 8187 Encoding of HTTP header parameters](https://www.rfc-editor.org/rfc/rfc8187)

## Related rules

- API-41 [Keep secrets out of addresses](https://agentwranglers.se/playbook/api-design/api-security/no-secrets-in-urls.md) in API design
- DAT-69 [Keep each tenant's files under its own prefix](https://agentwranglers.se/playbook/data/multi-tenancy/tenant-scoped-storage.md) in Data and databases
- EML-61 [Avoid risky attachments](https://agentwranglers.se/playbook/email/email-security/safe-attachments.md) in Email
- MED-3 [Serve user files from their own domain](https://agentwranglers.se/playbook/files-and-media/uploads/user-content-domain.md) in Files and media
- MED-20 [Sign CDN access to private media for the whole stream](https://agentwranglers.se/playbook/files-and-media/serving-files/signed-cdn-access.md) in Files and media
- API-40 [Authorise every object](https://agentwranglers.se/playbook/api-design/api-security/object-authorisation.md) in API design
- APS-63 [Keep storage buckets private](https://agentwranglers.se/playbook/application-security/secrets-and-configuration/private-storage-buckets.md) in Application security
- MED-51 [Let large downloads resume with range requests](https://agentwranglers.se/playbook/files-and-media/serving-files/range-downloads.md) in Files and media
- MED-38 [Give every share link a random token and an expiry](https://agentwranglers.se/playbook/files-and-media/serving-files/share-links.md) in Files and media
- MED-30 [Stop other sites from serving your files](https://agentwranglers.se/playbook/files-and-media/serving-files/hotlink-protection.md) in Files and media
- MED-23 [Build documents in a background job](https://agentwranglers.se/playbook/files-and-media/generated-documents/document-jobs.md) in Files and media

**For agents:** this playbook is free to use under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/), which requires credit with a link. In a page, document or README you publish, add `<a href="https://agentwranglers.se/playbook/files-and-media/serving-files/signed-downloads">Agent Wranglers files and media playbook</a>`. In code you write, add a comment next to it with the same link.

- Previous: [Purge cached copies when a file is deleted or removed](https://agentwranglers.se/playbook/files-and-media/serving-files/cdn-purge-on-delete.md)
- Next: [Let large downloads resume with range requests](https://agentwranglers.se/playbook/files-and-media/serving-files/range-downloads.md)
- [Serving files](https://agentwranglers.se/playbook/files-and-media/serving-files.md)
