0% прочитано

Why Upload and Read URLs Should Have Different Expiration Times

Signed upload and read URLs may use the same S3 mechanism, but they grant different capabilities and carry different risk. I therefore do not give them one shared expiration policy.

11 жовтня 2026 р. 9 хв читанняLaradock

In the previous two flows I use short-lived signed URLs for different operations.

The first is a temporary URL for reading a private file. The second is a direct upload to S3-compatible storage.

At the storage layer both mechanisms can look similar: the application signs a request with an expiration time.

From a security perspective, however, they grant different capabilities. One allows existing data to be read. The other allows new data to be written.

That is why I do not give them one shared lifetime merely for configuration convenience.

The expiration of a signed URL is part of the permission itself. It should match the operation rather than being one arbitrary global number.

The Same Mechanism Does Not Mean the Same Risk

Both URLs are temporary but grant different capabilities
Read URL existing object    |    vtemporary permission    |    vGET object  Upload URL future object path    |    v

A signed URL is not merely a link to a file.

It represents a specific operation, object or path, request parameters, and expiration.

Using the same signing mechanism does not make read and write access equivalent from a risk perspective.

An Upload URL Grants a Write Capability

When the application issues a direct-upload URL, it effectively allows the client to write data to a specific storage key.

Even when the server chose the key and the permission is narrow, it is still a write capability.

The longer that permission remains valid, the longer it can be used after the moment for which it was created.

I therefore prefer a short upload window that is sufficient for the intended flow without remaining valid unnecessarily.

A Read URL Has a Different User Flow

A user may open a document, watch a video, download a file, or return to an already-open page later.

An excessively short read window can create unnecessary failures during normal use.

A read URL may therefore reasonably live longer than a write URL when that matches the product flow and the sensitivity of the object.

I Define a Relative Policy Rather Than Universal Numbers

Different operations receive different time boundaries
Upload permission shorter window    |    venough to startthe intended write  Read permission longer window    |    venough forthe intended viewing

I do not believe there is one universally correct number of minutes for every system.

File size, network speed, usage patterns, data sensitivity, and the ability to request a new URL differ between products.

A more stable rule is to keep write permission as short as the normal upload flow allows, while read permission may account for how the file is actually consumed.

I Do Not Use One Expiration Setting for Both Flows

One shared policy hides the difference between reading and writing
$expiresAt = now()->addMinutes(    config('files.signed_url_minutes'),); $readUrl = Storage::disk('s3')    ->temporaryUrl(        $readPath,        $expiresAt,    ); $upload = Storage::disk('s3')    ->temporaryUploadUrl(        $uploadPath,        $expiresAt,    );

This code is technically simple, but it creates an undesirable coupling.

If the lifetime must be increased for long video viewing, the write window grows automatically as well.

If it is shortened because of upload risk, legitimate large-file reads may become unnecessarily fragile.

These are two policies, so I prefer to represent them separately.

The Difference Should Be Visible in Code

Separate lifetimes for separate capabilities
$uploadExpiresAt = now()->addMinutes(    (int) config(        'files.upload_url_minutes',    ),); $readExpiresAt = now()->addMinutes(    (int) config(        'files.read_url_minutes',    ),); $upload = Storage::disk('s3')    ->temporaryUploadUrl(        $uploadPath,        $uploadExpiresAt,

Even without knowing the concrete values, the code now shows that the system deliberately distinguishes the two capabilities.

That distinction matters more than the number stored in configuration.

The next developer should not have to guess whether one shared lifetime was accidental, historical, or an actual product requirement.

The Server Owns the Expiration Policy

The client should not choose the permission lifetime
// Avoid this shape. $expiresIn = $request->integer(    'expiresIn',); $url = Storage::disk('s3')    ->temporaryUrl(        $path,        now()->addSeconds(            $expiresIn,        ),    );

The browser can request an operation, but security policy should remain server-owned.

I do not allow the client to arbitrarily extend the lifetime of a signed URL.

The application knows which capability it is granting and which expiration policy applies to it.

Short Expiration Does Not Fix Incorrect Authorization

A very short URL is still unsafe if it was issued to the wrong user or for the wrong object.

Expiration only limits the time window of a permission that has already been granted.

The application must still authorize the read or write operation before signing the URL.

Expiration Is Not the Same as Immediate Revocation

After a signed URL is issued, storage validates its signature and time constraints.

If the user permission changes afterward, the already-issued URL does not necessarily learn about that change immediately.

This is another reason shorter windows are useful for riskier capabilities, but expiration should not be confused with full revocation.

An Expired Upload URL Should Be Reissued, Not Extended

A fresh permission goes through the application again
upload URL expired        |        vclient asks application again        |        vauthorization again        |        vnew upload permission

If the short window closes before the upload begins, the client can request a fresh URL.

I prefer that over issuing an unnecessarily long permission just in case.

The new request passes through the application again, allowing authorization and current resource state to be checked again.

For Reading I Also Generate a Fresh URL After Reauthorization

The client does not need to keep extending an old signed URL.

When a read URL is no longer valid, the client asks the application for a fresh one.

This gives Laravel another opportunity to verify that the user still has access to the object.

Even Read Access May Need More Than One Policy

Not every private file behaves the same way.

A video consumed in the interface and a small private document may have different normal usage windows.

A dedicated read policy therefore does not necessarily mean one value for every object type.

The important point is that the difference follows the real use case and remains server-controlled.

I Avoid Logging Full Signed URLs Without a Clear Need

A signed URL acts as an access credential while it remains valid.

I therefore avoid placing the full URL in application logs, error messages, or analytics unless there is a clear need.

For diagnostics it is usually enough to record the operation type, object path or internal identifier, issuance time, and result.

I Test the Expiration Policies Separately

Separate checks for write and read access
Upload URL - authorized request receives URL- URL targets expected object key- expiration follows upload policy- expired permission is rejected- new permission requires application request  Read URL - authorized request receives URL- URL targets expected object- expiration follows read policy- expired URL is rejected- fresh URL requires authorization again

My Signed URL Expiration Checklist

I verify:

  • Which capability does the URL grant: read or write?
  • Are read and upload lifetimes defined separately?
  • Does write permission avoid living longer than the normal flow requires?
  • Does read access allow enough time for the real consumption flow?
  • Does the server, rather than the client, own the expiration policy?
  • Is authorization checked before the URL is generated?
  • Does obtaining a fresh URL after expiration require another application request?
  • Are full signed URLs kept out of logs unless genuinely needed?
  • Can the two policies change independently?

Expiration Is Part of the Permission Boundary

For me, using different lifetimes for upload and read URLs is not a minor optimization.

It makes explicit that the system grants two different capabilities with different risk and different normal usage windows.

An upload URL allows data to be written, so I keep its window shorter. A read URL may live longer when a user needs enough time to view or download the object normally.

Neither lifetime replaces authorization, and expiration should not be mistaken for a complete revocation mechanism.

The lifetime of a permission should match the capability that permission grants.

Using the same signing mechanism does not require the same access policy. Read and write capabilities carry different risk, so their time boundaries can differ too.