Skip to content
LaravelBlog

livewire/livewire: Signed upload/preview URLs no longer break behind untrusted proxies

LaravelBlogBot
LaravelBlogBot

Contributed by MaxencePaulin

Part of the livewire v4.4.7 release

If you serve Livewire behind a reverse proxy or load balancer that terminates TLS, you may have hit a painful failure mode: file uploads and image previews suddenly fail with 401 responses, even though the signed URLs Livewire generates look perfectly fine. PR #10574 fixes exactly that (it closes #10573). Instead of signing and validating absolute URLs — which bakes a specific scheme, host, and port into the hash — Livewire now signs its local upload and preview URLs relatively and validates them with Laravel's hasValidRelativeSignature().

What was breaking behind proxies

Laravel's URL signer builds URLs from the origin your app declares publicly. Behind an SSL-terminating proxy you likely force the scheme:

// e.g. in AppServiceProvider::boot()
URL::forceScheme('https');

So signing produces https://example.com/livewire/upload-file?...&signature=.... But when the browser posts the file, request()->hasValidSignature() rebuilds the URL from the raw request — the internal scheme, host, and port Laravel actually sees, like http://10.0.4.12:8080. Different inputs, different hash, so the check fails and every upload or preview request 401s even though nothing is genuinely invalid.

The only workaround was teaching Laravel to trust the proxy's forwarded headers:

protected $proxies = '*'; // or specific IPs, or REMOTE_ADDR

That works, but it's a security-relevant configuration decision that not every deployment can or wants to make just to let uploads function.

The fix: sign relative, validate relative

A new signedRoute() helper on GenerateSignedUploadUrl does the signing:

// Signs relative so the scheme can't mismatch behind a proxy; re-absolutized
// for a normal-looking URL. Validate with `hasValidRelativeSignature()`.
public function signedRoute($name, $expiration, $parameters = [])
{
    $relative = URL::temporarySignedRoute($name, $expiration, $parameters, false);

    return URL::to($relative);
}

The false fourth argument tells Laravel to skip absolutizing before hashing, so the signature covers only the path and query. URL::to() then re-adds the scheme and host, so callers still receive a normal absolute URL — the browser sees no difference.

The two controllers flip to Laravel's method purpose-built for this case:

// FileUploadController & FilePreviewController
// before: abort_unless(request()->hasValidSignature(), 401);
abort_unless(request()->hasValidRelativeSignature(), 401);

hasValidRelativeSignature() verifies the signature and expiry over the relative URL and ignores scheme/host/port mismatches. TemporaryUploadedFile::temporaryUrl() now also routes through the facade's signedRoute() (whose docblock gained the new @method), so upload and preview URLs share one signing path — on this branch forLocal() is its only caller so far, but flows on main like chunks and multipart share it too. forS3() is untouched: AWS SigV4 presigned URLs never go through Laravel's signer.

Does dropping the host weaken the signature?

The useful guarantees remain. The signature still covers the route path, all query parameters, and the expiration timestamp, and it's still keyed to your app key — a URL can't be forged, altered, or extended without it. What's dropped is an incidental binding to scheme, host, and port, which is precisely the binding that rejected legitimate traffic behind proxies. If your threat model genuinely requires origin pinning, keeping TrustProxies configured with specific proxy IPs still works; the fix doesn't conflict with it.

The PR backs this up at two levels: a browser test that signs an upload URL under https, rewrites it to http, and confirms the upload still completes, plus a unit test doing the same for preview URLs — both failing before the fix. The author notes they couldn't run a real TLS proxy locally, so the tests simulate the scheme swap rather than an actual proxy; it exercises the same code path.

Upgrading from v3.8.10

  • Standard usage — WithFileUploads, wire:model, $photo->temporaryUrl() — needs no changes. Generated URLs are still absolute, and behavior is identical wherever origins already matched.
  • If you set TrustProxies only to unbreak uploads, you can now tighten it (for example from '*' back to specific IPs) or drop it entirely.
  • If you sign URLs to livewire.upload-file / livewire.preview-file yourself, sign relatively too (URL::temporarySignedRoute($name, $expiry, $params, false)); an absolutely signed hash won't match the new relative validation.
  • Short-lived URLs generated just before deploying may 401 once during the switchover; they expire within minutes and resolve themselves.
  • If you subclass GenerateSignedUploadUrl and call parent::forLocal(), you inherit the fix automatically.

Takeaways

  • 401s on uploads and previews behind untrusted TLS-terminating proxies are fixed at the source, not via TrustProxies gymnastics.
  • Signing is now relative (path + query + expiry); validation uses hasValidRelativeSignature().
  • Returned URLs remain absolute, and S3 upload flows are unaffected.
  • Hand-rolled signing of these routes must switch to relative signing to keep validating.

Category: Fix (3)

Sources

More from this release

Related Articles