Presigned URLs Explained: When, Why, and How to Use Them Securely
Learn how presigned URLs work, when to use them for private file downloads and direct uploads, and how to avoid the security pitfalls that lead to data leaks.
The Problem: Giving Access Without Giving Credentials
You have a private file in cloud storage. A user needs to download it. The file shouldn't be public, but you also don't want to proxy every byte through your server — that defeats the purpose of having a CDN or object storage in the first place.
This is the problem presigned URLs solve. They let you generate a temporary, self-contained URL that grants access to a specific resource for a limited time — without exposing your credentials, without opening the file to the public, and without routing traffic through your backend.
Presigned URLs are used everywhere: SaaS apps serving private user uploads, e-commerce platforms generating invoice downloads, healthcare portals sharing medical records, and developer tools enabling direct file uploads from the browser. If you've ever clicked a "Download" button and noticed a long URL with query parameters like X-Amz-Signature, you've used a presigned URL.
This guide explains the mechanism from first principles, walks through common use cases, highlights the security mistakes that lead to real data leaks, and provides implementation examples for both S3 and files.link's signed URL system.
How Presigned URLs Work: The Mechanics
A presigned URL is a regular HTTP URL with cryptographic query parameters appended. These parameters prove that someone with valid credentials authorized access to a specific resource at a specific time, for a limited duration.
Here's what an S3 presigned URL looks like, broken into its components:
https://my-bucket.s3.us-east-1.amazonaws.com/private/report.pdf
?X-Amz-Algorithm=AWS4-HMAC-SHA256
&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE/20260219/us-east-1/s3/aws4_request
&X-Amz-Date=20260219T120000Z
&X-Amz-Expires=600
&X-Amz-SignedHeaders=host
&X-Amz-Signature=a1b2c3d4e5f6...Each parameter serves a purpose:
- X-Amz-Algorithm — the signing algorithm (always HMAC-SHA256 for AWS Signature V4)
- X-Amz-Credential — identifies which access key signed the URL, scoped to a date, region, and service
- X-Amz-Date — when the signature was generated
- X-Amz-Expires — how many seconds the URL remains valid (600 = 10 minutes)
- X-Amz-SignedHeaders — which HTTP headers are included in the signature (at minimum,
host) - X-Amz-Signature — the HMAC-SHA256 hash that ties everything together
The cryptographic mechanism is straightforward. Your server takes the request details — the HTTP method, the object path, the expiration time, and several other fields — concatenates them into a "canonical request" string, then signs that string using your secret access key. The result is the signature appended to the URL.
When someone uses the URL, the storage service reconstructs the same canonical request from the incoming HTTP request, computes the expected signature using the same secret key, and compares. If they match and the URL hasn't expired, access is granted. If anything has been tampered with — the path, the expiration, any signed header — the signatures won't match and the request is rejected.
The key insight: the secret key never appears in the URL. The URL contains only the *result* of signing with the key. Anyone who intercepts the URL can use it (until it expires), but they cannot modify it or generate new URLs for other resources.
When to Use Presigned URLs
Presigned URLs fit three primary use cases:
Private file downloads
The most common scenario. Your backend checks authorization (is this user allowed to access this file?), generates a presigned URL pointing to the private object, and returns the URL to the client. The client downloads directly from cloud storage — no bytes flow through your server. This is exactly how private file storage works in practice.
Direct browser uploads
Instead of routing file uploads through your backend, generate a presigned PUT or POST URL and hand it to the client. The browser uploads directly to cloud storage, saving your server's bandwidth and CPU. This is critical for large files — a 500 MB video upload shouldn't consume your API server's memory and network connection for several minutes.
Temporary sharing
Generate a time-limited URL and send it via email, Slack, or any channel. The recipient can access the file without authenticating, but the URL stops working after the expiration window. This is how "share a link" features work in tools like Dropbox, Google Drive, and files.link.
Webhook and third-party integrations
When an external service needs to fetch a file from your storage — say, a PDF processor or a video transcoding service — you can pass a presigned URL instead of uploading the file to them. They fetch it directly, and the URL expires after the job completes.
Security Pitfalls: How Presigned URLs Get You in Trouble
Presigned URLs are secure by design, but they're routinely misused in ways that create real vulnerabilities. Here are concrete scenarios to watch for.
Pitfall 1: Overly long expiration times
Setting Expires to 86400 (24 hours) or higher because "it's easier" means any leaked URL remains exploitable for that entire window. In 2024, a healthcare SaaS was found serving patient documents with 7-day presigned URLs. A single URL pasted into a support ticket gave anyone with access to the ticket system a week-long window to download medical records.
Rule of thumb: set expiration to the minimum viable duration. For downloads triggered by a button click, 60-300 seconds is usually sufficient. For direct uploads, match it to the expected upload time plus a margin — 600-900 seconds for large files.
Pitfall 2: Logging presigned URLs in plaintext
Your access logs, error tracking (Sentry, Datadog), and even browser history will capture the full URL including the signature. Anyone with access to those logs effectively has access to the file until the URL expires.
This is particularly dangerous when URLs end up in:
- Nginx or Apache access logs (default behavior)
- Application error logs when a download fails
- Browser history and the
Refererheader when navigating away from the page - Analytics tools that capture page URLs
Mitigation: treat presigned URLs like short-lived secrets. Redact the signature parameter from logs. Use Referrer-Policy: no-referrer on pages that display presigned URLs. Set HTTP-only redirect flows instead of exposing the URL to client-side JavaScript where possible.
Pitfall 3: No IP or network restriction
A presigned URL is a bearer token — anyone who has it can use it from anywhere. If a URL leaks (via logs, shared screenshots, or a forwarded email), there's no secondary check.
AWS supports adding IP conditions to presigned URLs by issuing STS-federated credentials with a session policy that includes an aws:SourceIp condition, but this is complex and rarely implemented. A more practical approach: keep expiration times short so the window of vulnerability is small.
Pitfall 4: Generating presigned URLs for write or delete operations
Generating a presigned URL for a DELETE or PUT operation and accidentally exposing it is worse than leaking a read URL. An attacker with a presigned DELETE URL can destroy your data. Only generate presigned URLs for the minimum necessary HTTP method — almost always GET for downloads and PUT for uploads.
Pitfall 5: Reusing presigned URLs across sessions
Some developers cache presigned URLs to avoid regenerating them on every request. If a user's access is revoked but a cached presigned URL hasn't expired, they can still access the file. Presigned URL expiration should be shorter than your access revocation window.
Implementation: Generating Presigned URLs in Node.js
AWS S3 — download URL
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
const s3 = new S3Client({ region: "us-east-1" });
async function generateDownloadUrl(bucket, key) {
const command = new GetObjectCommand({
Bucket: bucket,
Key: key,
});
// URL expires in 5 minutes (300 seconds)
const url = await getSignedUrl(s3, command, { expiresIn: 300 });
return url;
}AWS S3 — upload URL
// Uses s3 and getSignedUrl from the example above
import { PutObjectCommand } from "@aws-sdk/client-s3";
async function generateUploadUrl(bucket, key, contentType) {
const command = new PutObjectCommand({
Bucket: bucket,
Key: key,
ContentType: contentType,
});
// 15 minutes for upload
const url = await getSignedUrl(s3, command, { expiresIn: 900 });
return url;
}
// Client-side upload using the presigned URL
// fetch(url, { method: "PUT", body: file, headers: { "Content-Type": contentType } })files.link — private file access
With files.link, presigned URLs are generated automatically for files stored in private folders. When you request a file through the API, the response includes a time-limited signed URL:
// Files in private folders automatically get signed URLs
const response = await fetch("https://api.files.link/v1/files/FILE_ID", {
headers: { Authorization: "YOUR_API_KEY" },
});
const { signedUrl } = await response.json();
// signedUrl is a CloudFront signed URL, valid for 10 minutes
// Redirect the user or return it to the clientNo manual key management, no SDK configuration, no expiration math. The service handles signing, expiration, and CloudFront key rotation. For a deeper walkthrough, see the signed URL file access guide.
Provider Comparison: Presigned URL Features
| Feature | AWS S3 | Google Cloud Storage | Azure Blob Storage | files.link |
|---|---|---|---|---|
| Signing mechanism | HMAC-SHA256 (SigV4) | RSA-SHA256 or HMAC | HMAC-SHA256 (SAS tokens) | CloudFront RSA signatures |
| Max expiration | 7 days | 7 days | No hard limit (policy-based) | 10 minutes (fixed) |
| Default expiration | 15 minutes (SDK default) | 15 minutes | Varies by SAS type | 10 minutes |
| Upload via presigned URL | Yes (PUT or POST policy) | Yes (PUT or POST policy) | Yes (SAS with write permission) | Via API (direct upload endpoint) |
| IP restriction support | Via STS policy conditions | Not natively supported | Yes (IP range in SAS) | Not currently supported |
| SDK required to generate | Yes (@aws-sdk/s3-request-presigner) | Yes (@google-cloud/storage) | Yes (@azure/storage-blob) | No (API returns signed URLs) |
| Key management | IAM access keys or roles | Service account keys | Storage account keys or AAD | Managed (no user-facing keys) |
| CDN-signed URLs | Separate CloudFront signing | Separate Cloud CDN signing | Via Azure CDN token auth | Built-in (CloudFront) |
Security Checklist for Presigned URLs
Use this as a review checklist before deploying presigned URLs in production:
Expiration
- Set the shortest expiration that works for your use case (60-300s for downloads, 600-900s for uploads)
- Never exceed 24 hours unless you have a documented reason
- For files.link, the 10-minute fixed expiration handles this automatically
Logging and leakage
- Redact or omit the
Signaturequery parameter from access logs - Set
Referrer-Policy: no-referreron pages that trigger presigned URL downloads - Do not log full presigned URLs in application error tracking
- Use server-side redirects (HTTP 302) instead of exposing the URL in client-side JavaScript when possible
Access control
- Always verify user authorization on your server before generating a presigned URL
- Use the minimum required HTTP method (GET for downloads, PUT for uploads — never DELETE)
- Set expiration shorter than your access revocation window
- Do not cache presigned URLs longer than their expiration time
Infrastructure
- Rotate signing keys on a regular schedule (for S3: rotate IAM access keys; for CloudFront: rotate key pairs)
- Use separate IAM credentials scoped to only the buckets and operations needed for signing
- If using CloudFront signed URLs, prefer signed URLs over signed cookies for single-file access
- Monitor for unusual access patterns on presigned URLs (e.g., a single URL used from many IPs)
Upload-specific
- Set
Content-Typerestrictions on upload presigned URLs to prevent malicious file type uploads - Validate uploaded files server-side after upload — presigned URLs do not guarantee file content safety
- Set
Content-Lengthconditions to prevent abuse (e.g., uploading a 10 GB file to a URL meant for a profile picture)
For a complete guide to implementing private file storage with presigned URLs, including folder-level visibility controls and automatic signed URL generation, see the files.link documentation on signed URL file access.
Conclusion
Presigned URLs are one of those cloud primitives that seem simple on the surface but have real security implications when misused. The mechanism is elegant — cryptographic proof of authorization embedded in a URL, no credentials exposed, no server-side proxying required.
The most important takeaways:
- Keep expiration times as short as possible. This is the single most effective security measure.
- Treat presigned URLs like short-lived secrets. They grant access to whoever holds them.
- Don't log them in plaintext. Your access logs, error trackers, and analytics are all potential leak vectors.
- Verify authorization before signing. The presigned URL itself doesn't know who should have access — that's your application's job.
If you want presigned URLs without managing signing keys, CloudFront key pairs, and expiration logic yourself, files.link handles signed URL generation automatically for private files. Set a folder to private, and every file access returns a 10-minute CloudFront signed URL. But whether you build it yourself with the S3 SDK or use a managed service, the security fundamentals in this guide apply either way.