5.9.7. Presigned URLs
Table of Contents
Why Presigned URLs Matter
When you work with file storage services like Amazon S3 or other S3 compatible storage, you often face a problem:
- Backend must control who can access or upload files.
- Files themselves are stored in a separate storage service.
- Clients, browsers, or mobile apps should not know your secret storage keys.
Presigned URLs solve this by letting your backend say:
“Here is a special URL that lets you upload or download exactly this file, in this way, for a short time. After that it stops working.”
You keep full control in your backend, but the browser talks directly to the storage service.
What Is a Presigned URL?
A presigned URL is a normal HTTP URL with a hidden “signature” added as query parameters.
The signature is created by your backend using your secret access keys. The storage service can verify this signature.
So the URL both:
- Identifies what is allowed, for example “PUT a file to
my-bucket/user-123/avatar.png”. - Proves that your backend approved this action, because it was signed using your secret key.
A presigned URL usually specifies:
- Action (method), for example
GETto download orPUTto upload. - Bucket or container name.
- Object key (file path) inside that bucket.
- Expiration time.
- Sometimes allowed headers or content type.
Key rule: A presigned URL is temporary authorization for a specific operation on a specific object. After it expires, it must not work anymore.
Example of a presigned URL (shortened):
https://my-bucket.s3.amazonaws.com/user-123/avatar.png
?X-Amz-Algorithm=AWS4-HMAC-SHA256
&X-Amz-Credential=AKIA...
&X-Amz-Date=20260828T120000Z
&X-Amz-Expires=300
&X-Amz-Signature=abcd1234...
&X-Amz-SignedHeaders=host
Clients just use it as a normal URL, for example in a browser or curl.
Download vs Upload Presigned URLs
You can use presigned URLs for:
| Purpose | HTTP method | Who calls the URL | Data direction |
|---|---|---|---|
| Download file | GET | Browser or mobile client | Storage → Client |
| Upload file | PUT or POST form | Browser or mobile client | Client → Storage |
The difference is important:
- Download presigned URL
Backend says: “Client may read this object for a short time.” - Upload presigned URL
Backend says: “Client may create or overwrite this object, possibly with limited size and type, for a short time.”
You usually:
- Ask the backend for a presigned URL.
- Backend returns JSON with that URL (and sometimes additional form fields).
- Client uses the URL directly against the storage server.
Example: Download flow
- Client:
GET /files/123/download-link - Backend: checks permissions for file 123 and user.
- Backend: generates presigned
GETURL for S3. - Backend: returns JSON:
{
"url": "https://my-bucket.s3.amazonaws.com/...signed..."
}- Client: redirects browser to that URL or uses it with
fetch.
Example: Upload flow
- Client:
POST /files/upload-linkwith JSON specifying file name, type, etc. - Backend: validates user and file info.
- Backend: generates presigned
PUTURL. - Backend: returns JSON with the URL.
- Client: uploads file directly to storage with
PUT:
curl -X PUT -T myfile.png "https://my-bucket.s3.amazonaws.com/...signed..."Security Properties and Best Practices
Presigned URLs are powerful, but you must use them carefully.
Expiration Time
The URL has an expiration, usually in seconds. For example, 300 seconds is 5 minutes.
Rule: Use the shortest expiration that still works for your use case.
Never create presigned URLs that last hours or days unless you absolutely need that behavior.
Common values:
| Use case | Typical expiration |
|---|---|
| File upload from browser | 5 to 15 minutes |
| One-time secure download | 5 to 60 minutes |
| Public-ish download (low risk) | 1 to 24 hours |
Longer expiration means:
- More convenient for clients.
- More time for an attacker to abuse a leaked URL.
Least Privilege per URL
Each presigned URL must be scoped as tightly as possible:
- Only one HTTP method (GET, PUT, POST).
- Only one object key (for example one file path).
- Only one bucket.
- Restricted headers or content type when possible.
Rule: A presigned URL must grant permission to one specific file operation, not to a folder or pattern.
Bad example:
- Use one presigned URL for multiple different files.
Good example:
- Generate a new presigned URL for each individual upload or download.
Never Expose Your Secret Keys
Clients must never see:
- Access keys
- Secret keys
- Environment variables
- Any code that can sign its own URLs
All signing happens in your backend only. The client sees only the result, the presigned URL.
Presigned URLs and Backend Responsibility
It might look like presigned URLs bypass your backend, but your backend is still where all decisions are made.
Your backend must answer these questions before generating a URL:
- Is this user authenticated?
- Does this user have permission to access this file?
- Does this user own the resource (for example “avatar for user 123”)?
- Is this file action allowed (upload, download, overwrite, delete)?
- Are there file constraints (size limit, allowed types)?
Example logic in plain steps:
- User calls
POST /users/me/avatar/upload-url. - Backend checks:
- User is logged in.
- File type and size requested are acceptable.
- Backend decides on the final storage path, for example
users/{user_id}/avatar.png. - Backend generates a presigned
PUTURL for that path with a short expiration. - Backend returns the URL to the client.
- Client uploads directly to storage.
- Optionally, backend later confirms that the upload succeeded, for example with a callback or a separate “mark as uploaded” endpoint.
Example Flow with S3 Compatible Storage
To make the idea concrete, here is a simple pattern using a generic S3 compatible storage.
1. Backend generates upload URL
Request:
POST /files/upload-url
Content-Type: application/json
{
"file_name": "photo.png",
"content_type": "image/png"
}Backend steps:
- Read user ID from authentication.
- Create a filepath like
users/{user_id}/photos/{uuid}.png. - Decide expiration, for example 600 seconds.
- Ask the storage SDK to generate a presigned
PUTURL for that key. - Return JSON:
{
"upload_url": "https://my-bucket.s3.amazonaws.com/users/123/photos/abcd-uuid.png?...signature...",
"file_path": "users/123/photos/abcd-uuid.png"
}2. Client uploads directly
Client uses any HTTP tool:
curl -X PUT \
-H "Content-Type: image/png" \
--upload-file ./photo.png \
"https://my-bucket.s3.amazonaws.com/users/123/photos/abcd-uuid.png?...signature..."Storage validates:
- URL not expired.
- Signature matches.
- Headers match what was signed, if restricted.
If OK, storage saves the object.
3. Backend uses the file path
Backend stores file_path in the database, related to the user or another entity. The database only contains paths like:
users/123/photos/abcd-uuid.pngWhenever a download link is needed, the backend:
- Checks permissions.
- Uses the stored path.
- Generates a new presigned
GETURL for that path. - Returns it to the client.
Common Pitfalls and How to Avoid Them
Pitfall 1: Too Long Expiration
If you use something like 1 day for all URLs, a leaked link is dangerous for much longer.
Avoid by:
- Using shorter expiration for sensitive content.
- Regenerating URLs if the user asks again.
Pitfall 2: Directly Using User-provided Paths
If a client can choose the storage key freely, they may overwrite other users’ files or important objects.
Avoid by:
- Backend must construct object keys itself.
- Use patterns that include user IDs or resource IDs.
- Ignore any client-provided bucket or path input.
Pitfall 3: Not Checking Permissions
If you generate presigned URLs just because a user knows a file ID, they could guess another user’s file ID and get access.
Avoid by:
- Mapping file IDs to owners or permissions in your database.
- Always validate that the authenticated user has rights to the file before signing a URL.
Pitfall 4: Mixing Public and Private Access
Sometimes a bucket is public, and you still generate presigned URLs. This reduces the benefit of access control.
Choose a clear strategy:
- Public content: maybe no need to presign, or use short lived presigned URLs to log accesses.
- Private content: bucket locked down, access only by presigned URLs or via backend streaming.
When to Use Presigned URLs vs Backend Proxy
There are two main patterns for serving or receiving files.
| Pattern | How it works | Pros | Cons |
|---|---|---|---|
| Presigned URL | Client ↔ Storage directly, URL signed by backend | Scalable, offloads traffic | Slightly more complex client logic |
| Backend proxy (no presign) | Client ↔ Backend ↔ Storage. Backend streams file | Simpler client API, more control | Backend handles all file traffic |
Presigned URLs are typically best when:
- Files are large.
- You expect many uploads or downloads.
- You want to reduce load on your backend.
Backend proxy is sometimes better when:
- Files are small and infrequent.
- You need heavy processing or strict logging in the backend.
- You want to avoid exposing storage URLs at all.
In many real systems you use both patterns, depending on the endpoint.
Summary
- A presigned URL is a normal URL with a server side signature that gives temporary permission for a specific file operation.
- Your backend generates presigned URLs, and clients use them directly with the storage service.
- Backend remains the authority that decides who can upload or download which file, and for how long.
- Use short expirations, least privilege, never expose secret keys, and always validate user permissions before generating a URL.
- Presigned URLs are especially useful for efficient file uploads and downloads with services like S3 or S3 compatible storage.
Views: 6
KAHIBARO