Cross-origin iFrames with Laravel
Maybe you already encountered one of these errors when creating a page that can be embedded inside an iframe:
- Blocked a frame with origin ... from accessing a frame with origin ...
- Unsafe JavaScript attempt to access frame with URL ...
- Invalid 'X-Frame-Options' header encountered when loading ...
They are due to browsers preventing to embed or access an iframe from an untrusted domain.
The solution presented in this article supposes that you have access to the server and the app where the iframe source is hosted.
This is for a Laravel application hosted on Laravel Forge, but it is applicable to most web applications running on Nginx or Apache.
Tweak the server config
First, we need to check that our server's config doesn't contain headers preventing from embedding the pages inside an iFrame.
The default Nginx config when a new site is provisioned by Laravel Forge contains an X-Frame-Options
setting that we are going to remove, so we can control this HTTP header inside our app.
In Laravel Forge, go to Sites
, then in the Apps
tab scroll down until the bottom of the page.
Then click on Edit Nginx Configuration
and comment out this line:
# add_header X-Frame-Options "SAMEORIGIN";
add_header X-XSS-Protection "1; mode=block";
add_header X-Content-Type-Options "nosniff";
Then you can save the config and restart Nginx.
On Apache, it will be like "Header always append X-Frame-Options SAMEORIGIN"
Create a Middleware
Since we removed this header from the server's config, we are now going to create a Middleware that allows us to control it.
For this, you can execute the following command to scaffold a new middleware called XFrameOptions
:
$ php artisan make:middleware XFrameOptions
And copy this code inside it:
<?php
namespace App\Http\Middleware;
use Closure;
class XFrameOptions
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
*
* @return mixed
*/
public function handle($request, Closure $next)
{
$response = $next($request);
$option = 'SAMEORIGIN';
// In this example, we are only allowing the third party to include the "iframe" route
// It's always better to scope this to a given route / set of routes to avoid any unattended security problems
if ($request->routeIs('iframe') && $xframeOptions = env('X_FRAME_OPTIONS', 'SAMEORIGIN')) {
if (false !== strpos($xframeOptions, 'ALLOW-FROM')) {
$url = trim(str_replace('ALLOW-FROM', '', $xframeOptions));
$response->header('Content-Security-Policy', 'frame-ancestors '.$url);
}
}
$response->header('X-Frame-Options', $xframeOptions);
}
}
We are both setting the
X-Frame-Options
and theContent-Security-Policy
headers becauseX-Frame-Options
should be ignored ifCSP frame-ancestors
is specified, but Chrome 40 & Firefox 35 ignore the frame-ancestors directive and follow theX-Frame-Options
header instead.
Only the iframe
route has been allowed in this example. Allowing all your app's routes to be embeddable inside iFrames can be a huge security risk, so you must be careful with what you allow to be included.
More information about the security risks associated with this can be found here.
You can now edit your .env
file in order to configure the X-Frame-Options
on a site-by-site basis, without having to restart the web server(s):
// .env
X_FRAME_OPTIONS=DENY
X_FRAME_OPTIONS="ALLOW-FROM https://google.fr"
That's it! Your page can now be embedded as an iFrame from a trusted third party :)