How to upload to S3 securely with presigned URLs
Never ship AWS access keys to the browser. The standard pattern is presigned upload URLs: your backend signs a short-lived URL using the access key, the browser uploads directly to S3 with that URL, and the credential never leaves the server. For files over 5 GB (or anything you can't afford to restart), use multipart upload — one presigned URL per part, uploaded in parallel.
Step-by-step.
- 01
Why not put the AWS key in the browser?
Anything in your browser bundle is public — your access key ends up readable to anyone who opens DevTools. Even with scoped IAM, leaking the key lets attackers do anything allowed by that IAM policy until you rotate. Presigned URLs avoid the problem entirely by signing on the server and handing the browser only a short-lived URL. - 02
Single-file upload: one presigned PUT URL
Your backend callsgetSignedUrlwithPutObjectCommand. The browser receives the URL and PUTs the file body directly to S3. The URL expires quickly (15 minutes is a good default) and only allows writing to the specific key.import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; import { PutObjectCommand } from '@aws-sdk/client-s3'; const url = await getSignedUrl( s3, new PutObjectCommand({ Bucket: 'my-bucket', Key: 'uploads/' + filename, ContentType: contentType, }), { expiresIn: 900 }, // 15 minutes ); - 03
Browser PUT with the URL
From the client,fetch(url, { method: 'PUT', body: file }). No access key in the page; no proxy through your server; the file bytes go browser → S3 directly. Pair withContentTypeheaders so the object lands with the right type for downloads later. - 04
Large files: multipart with presigned per-part URLs
Above ~100 MB, switch to multipart upload. Your backend callsCreateMultipartUpload, signs a presigned URL per part, and returns them to the client. The browser PUTs each part in parallel, collects ETags, and posts them back; your backend completes the upload. Failed parts retry independently. - 05
How S3 Viewer's upload uses this pattern
When you drop a file into S3 Viewer above 50 MB, the API callsCreateMultipartUploadand signs a presigned URL for each 25 MB part. The browser uploads parts directly to S3 in parallel via those URLs — your access key, encrypted at rest with RSA-4096 (PKCS1_OAEP, SHA-256) server-side, never travels. - 06
Restrict the presigned URL further
You can lock down the presigned URL with conditions: specific content-type, max size, exact key, expiry, source IP. Addingx-amz-meta-*conditions enforces metadata at upload time. The narrower the URL, the less damage if it leaks before expiry.
What's actually happening.
A presigned URL is a signed query-string version of an S3 request. The signature is computed on the server using your AWS access key, then the URL is handed to the browser — which can use it once, before it expires, for the exact operation it was signed for. For uploads, you sign a PutObjectCommand; for multipart, you sign one URL per part. The browser PUTs the bytes directly to S3, so the data path is browser → S3 with no proxy and no credential exposure. S3 Viewer's own upload UI uses this exact pattern: presigned per-part URLs above 50 MB, your encrypted credential signing them server-side, your browser never seeing the key.
Common questions.
How do I upload to S3 without exposing my AWS key?
Are presigned URLs secure?
Can I use presigned URLs for large file uploads?
What's the maximum size of a presigned upload?
Do presigned URLs work with Cloudflare R2?
How short should the expiry be?
Skip the CLI. Try it in the browser.
S3 Viewer turns the steps above into a single click. Open source, self-hostable, free for personal use.
Why teams pick this
More how-tos
Upload large files
Multipart upload — part sizes, parallelism, retries, and the 5 GB single-PUT cap that pushes you to multipart.
Share an S3 file
Presigned URL or workspace invite — when each is the right call, and why presigned links can't be revoked.
Download a file
Browser, AWS CLI, or presigned URL — three ways, with auto-inferred filenames and zero key exposure.