Skip to content
Pro Analyzer — Available with ShieldCI Pro

Clickjacking Protection Analyzer ​

Analyzer IDCategorySeverityTime To Fix
clickjacking🛡️ SecurityHigh5 minutes

What This Checks ​

Detects missing clickjacking protection in Laravel applications. Validates that the application is protected from clickjacking attacks through one of these methods:

  • FrameGuard middleware (sets X-Frame-Options: SAMEORIGIN by default)
  • Manual X-Frame-Options header in middleware or service providers (via ->header(), ->withHeaders(), or ->headers->set())
  • Content-Security-Policy frame-ancestors directive in middleware, config, or service providers
  • FrameGuard assigned via route-level middleware groups
  • spatie/laravel-csp package configuration with frame-ancestors
  • bepsvpt/secure-headers package configuration with X-Frame-Options or CSP
  • Server-level protection via .htaccess or nginx.conf

Also detects and warns about:

  • Deprecated ALLOW-FROM directive (not supported by modern browsers)
  • Overly permissive frame-ancestors * wildcard (effectively disables protection)
  • Content-Security-Policy-Report-Only used without an enforcing policy (logs violations but does not block framing)

Why It Matters ​

  • UI Redressing Attacks: Attackers can embed your application in an invisible iframe and trick users into clicking on elements they can't see
  • Credential Theft: Users can be tricked into entering credentials on what appears to be your site but is actually an attacker's overlay
  • Unauthorized Actions: Clickjacking can trick authenticated users into performing actions like changing passwords, transferring money, or deleting data
  • Session Hijacking: Attackers can capture user interactions and session data through iframe embedding

Clickjacking (also known as "UI redressing") occurs when an attacker embeds your application in an invisible iframe on a malicious website. When users think they're clicking on the attacker's page, they're actually clicking on your application, potentially performing unauthorized actions while authenticated.

How to Fix ​

Quick Fix (2 minutes) ​

php
// bootstrap/app.php
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Middleware;

return Application::configure(basePath: dirname(__DIR__))
    ->withMiddleware(function (Middleware $middleware) {
        // ✅ Add FrameGuard to web middleware
        $middleware->web(append: [
            \Illuminate\Http\Middleware\FrameGuard::class,
        ]);
    })
    ->create();
php
// app/Http/Kernel.php
protected $middlewareGroups = [
    'web' => [
        \App\Http\Middleware\EncryptCookies::class,
        \Illuminate\Session\Middleware\StartSession::class,
        \Illuminate\Http\Middleware\FrameGuard::class,  // ✅ Add this
    ],
];

Proper Fix (5 minutes) ​

Option 1: FrameGuard Middleware (Recommended)

The simplest approach is using Laravel's built-in FrameGuard middleware, which automatically sets the X-Frame-Options: SAMEORIGIN header.

php
// bootstrap/app.php
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Middleware;

return Application::configure(basePath: dirname(__DIR__))
    ->withMiddleware(function (Middleware $middleware) {
        // Add FrameGuard to web middleware group
        $middleware->web(append: [
            \Illuminate\Http\Middleware\FrameGuard::class,
        ]);
    })
    ->withExceptions(function (Exceptions $exceptions) {
        //
    })
    ->create();
php
// app/Http/Kernel.php
protected $middlewareGroups = [
    'web' => [
        \App\Http\Middleware\EncryptCookies::class,
        \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
        \Illuminate\Session\Middleware\StartSession::class,
        \Illuminate\View\Middleware\ShareErrorsFromSession::class,
        \App\Http\Middleware\VerifyCsrfToken::class,
        \Illuminate\Http\Middleware\FrameGuard::class,  // ✅ Add FrameGuard
        \Illuminate\Routing\Middleware\SubstituteBindings::class,
    ],
];

Option 2: Manual X-Frame-Options Header

Set the header manually in middleware for more control:

php
// app/Http/Middleware/SecurityHeaders.php
namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class SecurityHeaders
{
    public function handle(Request $request, Closure $next)
    {
        $response = $next($request);

        // ✅ Prevent clickjacking - only allow same origin
        $response->headers->set('X-Frame-Options', 'SAMEORIGIN');

        // Or deny all framing
        // $response->headers->set('X-Frame-Options', 'DENY');

        return $response;
    }
}

Register the middleware:

php
// Laravel 10+ - bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
    $middleware->web(append: [
        \App\Http\Middleware\SecurityHeaders::class,
    ]);
})

// Laravel 9 - app/Http/Kernel.php
protected $middlewareGroups = [
    'web' => [
        \App\Http\Middleware\SecurityHeaders::class,
    ],
];

Option 3: Content-Security-Policy with frame-ancestors

Use the more modern CSP approach for better control:

php
// app/Http/Middleware/SecurityHeaders.php
namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class SecurityHeaders
{
    public function handle(Request $request, Closure $next)
    {
        $response = $next($request);

        // ✅ Modern CSP approach
        $csp = "default-src 'self'; frame-ancestors 'self'";
        $response->headers->set('Content-Security-Policy', $csp);

        // For broader browser support, also set X-Frame-Options
        $response->headers->set('X-Frame-Options', 'SAMEORIGIN');

        return $response;
    }
}

Browser compatibility note: Content-Security-Policy: frame-ancestors is the modern standard and takes precedence in all current browsers. For maximum compatibility with legacy browsers (Internet Explorer), keep both X-Frame-Options and the CSP header, as shown above.

Option 4: Configuration-Based Approach

Store CSP configuration in a config file:

php
// config/security.php
return [
    'csp' => [
        'directives' => [
            'default-src' => ["'self'"],
            'frame-ancestors' => ["'self'"],
            'script-src' => ["'self'", "'unsafe-inline'"],
        ],
    ],

    'headers' => [
        'X-Frame-Options' => 'SAMEORIGIN',
    ],
];

Apply headers from config:

php
// app/Http/Middleware/SecurityHeaders.php
public function handle(Request $request, Closure $next)
{
    $response = $next($request);

    // Apply headers from config
    foreach (config('security.headers', []) as $header => $value) {
        $response->headers->set($header, $value);
    }

    // Build and apply CSP
    $directives = config('security.csp.directives', []);
    if (!empty($directives)) {
        $csp = $this->buildCspHeader($directives);
        $response->headers->set('Content-Security-Policy', $csp);
    }

    return $response;
}

private function buildCspHeader(array $directives): string
{
    $parts = [];
    foreach ($directives as $directive => $values) {
        $parts[] = $directive . ' ' . implode(' ', $values);
    }
    return implode('; ', $parts);
}

Option 5: Using Response::macro() for Reusable Security Headers

php
// app/Providers/AppServiceProvider.php
namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Response;

class AppServiceProvider extends ServiceProvider
{
    public function boot()
    {
        // Create a macro for secure responses
        Response::macro('withSecurityHeaders', function ($value = null) {
            /** @var \Illuminate\Http\Response $this */
            return $this->withHeaders([
                'X-Frame-Options' => 'SAMEORIGIN',
                'Content-Security-Policy' => "default-src 'self'; frame-ancestors 'self'",
                'X-Content-Type-Options' => 'nosniff',
                'X-XSS-Protection' => '1; mode=block',
            ]);
        });
    }
}

// Usage in controllers
public function index()
{
    return response()
        ->view('dashboard')
        ->withSecurityHeaders();
}

Option 6: Environment-Based CSP Configuration

Store CSP values in a config file (never call env() directly in middleware, as it returns null after php artisan config:cache):

php
// config/security.php
return [
    'csp_frame_ancestors' => env('CSP_FRAME_ANCESTORS', "'self'"),
    'csp_default_src' => env('CSP_DEFAULT_SRC', "'self'"),
];
ini
# .env
CSP_FRAME_ANCESTORS="'self' https://trusted-partner.com"
CSP_DEFAULT_SRC="'self'"
php
// app/Http/Middleware/SecurityHeaders.php
public function handle(Request $request, Closure $next)
{
    $response = $next($request);

    // ✅ Read from config — works correctly after config:cache
    $frameAncestors = config('security.csp_frame_ancestors', "'self'");
    $defaultSrc = config('security.csp_default_src', "'self'");

    $csp = "default-src {$defaultSrc}; frame-ancestors {$frameAncestors}";
    $response->headers->set('Content-Security-Policy', $csp);

    return $response;
}

Advanced Configuration ​

Allowing Specific Trusted Partners:

php
// app/Http/Middleware/SecurityHeaders.php
public function handle(Request $request, Closure $next)
{
    $response = $next($request);

    // Trusted partners that can embed our app
    $trustedPartners = [
        'https://partner1.com',
        'https://partner2.com',
    ];

    $frameAncestors = implode(' ', array_merge(["'self'"], $trustedPartners));
    $csp = "default-src 'self'; frame-ancestors {$frameAncestors}";

    $response->headers->set('Content-Security-Policy', $csp);

    // For older browser support
    $response->headers->set('X-Frame-Options', 'SAMEORIGIN');

    return $response;
}

Route-Specific Exceptions:

php
// app/Http/Middleware/ClickjackingProtection.php
public function handle(Request $request, Closure $next)
{
    $response = $next($request);

    // Allow framing from specific trusted origins for embed/widget routes
    if ($request->is('embed/*') || $request->is('widget/*')) {
        // ✅ CSP frame-ancestors for precise cross-origin control (ALLOW-FROM is deprecated)
        $response->headers->set(
            'Content-Security-Policy',
            "frame-ancestors 'self' https://trusted-partner.com"
        );
        // Note: X-Frame-Options is omitted for embed routes to avoid conflicts
    } else {
        // ✅ Strict for all other routes
        $response->headers->set('X-Frame-Options', 'DENY');
        $response->headers->set('Content-Security-Policy', "frame-ancestors 'none'");
    }

    return $response;
}

References ​