Cross-origin iFrames with Laravel

Cross-origin iFrames with Laravel Image

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.

this is an image

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 the Content-Security-Policy headers because X-Frame-Options should be ignored if CSP frame-ancestors is specified, but Chrome 40 & Firefox 35 ignore the frame-ancestors directive and follow the X-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 :)