Michiel Kempen • Generate HTTPS URLs when running Laravel behind a proxy
source link: https://michielkempen.com/blog/generate-https-urls-when-running-laravel-behind-a-proxy
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Generate HTTPS URLs when running Laravel behind a proxy #
Earlier today I got bitten by a nasty bug in my Laravel code. Why nasty? Well, it was one of those bugs that lay low during development but as soon as you push to production they set your whole application on fire. 🔥
My immediate reaction was:
However, after some debugging, the problem turned out to be a mix of both Dev and Ops.
Context #
Because I run my Laravel application behind a load balancer that terminates TLS, requests are forwarded from my load balancer to my application on port 80. This means that my application has no clue whether it is served over HTTP
or HTTPS
.
As a result, whenever I use the url()
helper in my Laravel application — for example, to include an image in my Blade views — the generated URL uses the HTTP
scheme. This works fine in development, but as soon as your application is served in production over HTTPS
, the HTTP
links are marked as mixed content by modern web browsers.
To solve this problem, I have had the following piece of code in my Laravel applications for years:
<?php
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\URL;
class AppServiceProvider extends ServiceProvider
{
public function boot()
{
if(config('app.env') == 'production') {
URL::forceScheme('https');
}
}
}
These three lines of code tell the url()
helper to always use the HTTPS
scheme whenever the application is running in production.
As a result, url('/img/icon.jpg')
translates to:
http://example.com/img/icon.jpg
in developmenthttps://example.com/img/icon.jpg
in production
Perfect solution, right?
At least, that is what I thought... until today.
The problem #
Apart from generating regular URLs via the url()
helper, Laravel also allows you to generate signed URLs. These URLs have a signature hash appended to the query string that allows Laravel to verify that the URL has not been modified since it was created.
Signed URLs are especially useful for routes that are publicly accessible yet need a layer of protection. An example of such a URL is the public unsubscribe link that is included in newsletters.
You can create a signed URL to a named route as follows:
use Illuminate\Support\Facades\URL;
return URL::signedRoute('unsubscribe', ['user' => 1]);
In your controller you can then verify that an incoming request has a valid signature:
use Illuminate\Http\Request;
public function unsubscribe(Request $request)
{
if (! $request->hasValidSignature()) {
abort(401);
}
// ...
}
Under the hood, the signedRoute()
method respects the URL::forceScheme('https')
in the AppServiceProvider
and generates a secure HTTPS
URL in production.
https://example.com/unsubscribe/1
Laravel then creates a signature from this URL and appends it as a query parameter to the URL. The result looks something like this:
https://example.com/unsubscribe/1?signature=524e55e5154e2138278b9aa59172dddbb95a9e4f9c4d90ee0199e4a959a3fc64
The problem is that the URL::forceScheme('https')
has no impact on the hasValidSignature()
method. Therefore, the method sees the URL from the incoming request as:
http://example.com/unsubscribe/1
Since the schemes of the requested and the observed URL are different, their signatures are different as well. As a result, when Laravel compares the signatures to verify the validity of the request, it assumes that the requested URL has been tampered with and returns a 403
response.
And suddenly, my URL::forceScheme('https')
solution doesn't look so perfect anymore...
The solution #
When a request passes through a proxy, a lot of the information about the client gets replaced by information about the proxy. For example, the REMOTE_ADDR
header that normally contains the IP address of the client now contains the IP address of the proxy.
However, this information is not entirely lost. Many proxies forward the details of the original request in the form of X-Forwarded-*
headers.
Common headers include:
X-Forwarded-For
The IP address of the clientX-Forwarded-Host
The hostname used to access the site in the browserX-Forwarded-Proto
The scheme used by the clientX-Forwarded-Port
The port used by the client
This means that my assumption that Laravel has no clue about whether it is served over HTTP
or HTTPS
was wrong. It can actually figure this out, but it has to look at the X-Forwarded-Host
header.
By default, Laravel does not take the X-Forwarded-*
headers into account. But the TrustProxies
middleware that is included by default in any Laravel application can change that.
So, to solve the problem, I configured the TrustProxies
middleware to "trust" any proxy by setting $proxies = "*"
. This tells Laravel to look at the X-Forwarded-*
headers whenever it receives a request that is forwarded by a proxy.
<?php
use Fideloper\Proxy\TrustProxies as Middleware;
use Illuminate\Http\Request;
class TrustProxies extends Middleware
{
/**
* The trusted proxies for this application.
*
* @var string|array
*/
protected $proxies = "*";
/**
* The headers that should be used to detect proxies.
*
* @var string
*/
protected $headers = Request::HEADER_X_FORWARDED_ALL;
}
Conclusion #
Thanks to the TrustProxies
middleware, my Laravel application can now figure out by itself whether it should generate an HTTP
or HTTPS
URL, even when it is running behind a proxy. This works for both regular URLs and signed URLs.
And that piece of code that lived for years in the boot()
method of my AppServiceProvider
?
That I removed... since its functionality had become obsolete. 🗑
if(config('app.env') == 'production') {
URL::forceScheme('https');
}
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK