Manoj
←Back to Blog
·4 min read

S3 Presigned URLs: Uploading Files Without Exposing Credentials

Design a direct-to-S3 upload flow with short-lived presigned URLs, server-owned object keys, and post-upload validation.

awss3securityfile-uploads

S3 Presigned URLs: Uploading Files Without Exposing Credentials

Routing every uploaded file through an application server can make a simple feature unnecessarily heavy. The server spends time receiving bytes, forwarding them, and handling interrupted connections even when its real job is deciding whether the upload is allowed.

A presigned URL lets the application make that decision while the browser sends the file directly to S3. The useful separation is between authorizing an upload and transporting it.

Understand what the URL grants

A presigned URL authorizes a particular S3 request using the permissions of its signer. It is time-limited, but anyone holding it can use the capability it grants. Treat it as a bearer credential rather than an ordinary public link.

It is not inherently a one-time token. A URL can be reused before it expires, and temporary signing credentials can expire sooner than the requested URL lifetime. AWS documents those details in its presigned URL guide.

Keep the lifetime short enough for the intended operation and avoid putting signed URLs in analytics, application logs, or support screenshots.

Start with an authenticated request

The browser first asks your backend for upload permission. The backend checks the signed-in user, intended file category, account limits, and any project-specific authorization rules.

Do not let the browser select an arbitrary bucket or object path. Generate the key on the server and associate it with the user or record that owns the upload. A random filename does not replace authorization, but a server-owned key prevents accidental collisions and unexpected locations.

The flow can remain small:

  1. Create a pending upload record after authorization.
  2. Generate a key inside the permitted prefix.
  3. Return a short-lived URL and the pending record ID.
  4. Upload directly from the browser.
  5. Validate the stored object before marking the record usable.

Sign the operation on the server

This AWS SDK for JavaScript v3 example illustrates signing after authorization. The SDK packages are dependencies of the hypothetical upload service, not requirements for this portfolio.

import { randomUUID } from "node:crypto";
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";

const s3 = new S3Client({ region: "ap-south-1" });

const createUpload = async (authorizedUserId: string) => {
  const key = `incoming/${authorizedUserId}/${randomUUID()}.png`;
  const command = new PutObjectCommand({
    Bucket: "project-upload-bucket",
    Key: key,
    ContentType: "image/png",
  });

  const url = await getSignedUrl(s3, command, { expiresIn: 300 });
  return { key, url };
};

The signer needs the relevant S3 permission. The browser receives the URL rather than the signing credentials. The actual upload must match the signed request, including headers that participate in the signature. AWS provides further presigned upload examples.

The browser still needs error handling

For a PUT upload, send the file body with the expected content type and check the response status. Configure bucket CORS for the application's browser origin, method, and required headers.

CORS is a browser access rule, not authentication. A permissive CORS configuration does not make an otherwise unauthorized S3 request valid, and a restrictive one does not protect a leaked signed URL from non-browser clients.

If the upload fails after the URL expires, request a fresh URL through the authorized backend flow. Do not silently retry forever or assume a partially completed interaction means the object is ready.

Validate bytes before trusting metadata

A filename ending in .png and a declared image/png content type do not prove the object contains a safe image. Treat both as untrusted input.

For an avatar feature, keep the object pending while checking ownership, actual size, and decoded image format. Apply malware scanning or additional processing when the use case requires it. Do not expose unverified uploads through a trusted content path.

Client-side size checks improve feedback but are not an enforcement boundary. Choose an upload mechanism and server-side validation strategy appropriate to the limits you need. Clean up abandoned pending records and orphaned objects on a schedule.

Keep the application state explicit

Model pending, validated, and rejected states rather than treating a returned URL as a completed upload. That makes retries, cleanup, and user feedback much easier to reason about.

Direct uploads work best when the backend stays responsible for permission and lifecycle, even though it no longer carries the file itself.