← Back to case studies
CVE-2025-53833 CVSS 10.0 Critical PHP · Laravel Blade

LaRecipe Server-Side Template Injection — Pre-Auth RCE via Unsanitized Blade Rendering

LaRecipe renders markdown as Blade templates — user-controlled content passed to Blade::render() without sanitization enables pre-auth remote code execution on any server running a vulnerable version of the package.

CVSS 10.0 / 10.0
CWE CWE-1336 ( SSTI )
Project LaRecipe (PHP/Laravel)
Published July 2025
Fix commit c1d0d56889655ce5f2645db5acf0e78d5fc3b36b
// what happened
  • Affected: LaRecipe < patched version — PHP package that embeds Markdown documentation inside Laravel Blade views. Used by thousands of Laravel projects as an in-app documentation system.
  • Attack vector: LaRecipe's markdown renderer passes raw user content to Blade::render() without sanitizing template directives. An unauthenticated attacker can inject arbitrary Blade expressions — including {!! !!} raw-output tags — to execute PHP code on the server.
  • Requires: Nothing — pre-auth RCE. No credentials, no user interaction.
  • Impact: Full server takeover — read environment variables (DATABASE_PASSWORD, AWS_SECRET_KEY), pivot to adjacent services, drop a webshell.
  • Fix: Strip or sandbox Blade rendering of untrusted markdown content. Fix commit: saleem-hadad/larecipe@c1d0d568.

Base Score
10.0 Critical — Maximum severity
Attack Vector (AV)
Network — HTTP request, no local access required
Attack Complexity (AC)
Low — single HTTP request, no conditions to meet
Privileges Required (PR)
None — pre-auth RCE, no credentials required
User Interaction (UI)
None — victim doesn't need to interact
Scope (S)
Changed — RCE crosses application boundary to system
Confidentiality (C)
High — read all files, env vars, database credentials
Integrity (I)
High — modify files, deploy webshell, corrupt data
Availability (A)
High — RCE = full system takeover, denial of service
Vector String
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H → 10.0

src/LaRecipe.php — markdown rendering method Pre-fix — user markdown content passed directly to Blade::render() // LaRecipe converts Markdown documentation pages to Blade views. // The render() method takes a markdown string and compiles it as a Blade template. public function render($markdown, $recipe) { // $markdown contains raw user-authored documentation content. // $recipe holds metadata about the current documentation page. // VULNERABLE: markdown content is compiled as a Blade template // This means backtick template expressions like {{ }} and {!! !!} // are evaluated as PHP code before being returned to the browser. // If an attacker can control any portion of $markdown — even via // a documentation comment or a "TODO" in a markdown file — they // can inject a Blade expression that executes arbitrary PHP. // Example malicious markdown content: // ## My Note // Check the config at `{{ file_get_contents('/etc/passwd') }}` // // Or even simpler: // `{{ system($_GET['cmd']) }}` // Laravel's Blade compiler evaluates expressions before HTML encoding. // The {!! !!} syntax writes raw (unencoded) output — useful for attackers. $content = Blade::render($markdown, [ 'recipe' => $recipe, ]); return $content; }
From documentation page to full server compromise
1
Attacker identifies a LaRecipe installation — typically at /docs or /recipes routes. These render markdown content from files stored in resources/docs/ or from database-stored content.
2
Attacker injects a Blade template expression into any markdown content field: `{{ system($_GET['x']) }}`. Even if the markdown file is stored on disk, an attacker who can POST to any documentation endpoint (e.g., commenting, editing via admin panel, or crafting a specific URL) can achieve injection.
3
When LaRecipe calls Blade::render($markdown, ...), Laravel's Blade compiler evaluates the template expression. The {{ }} expression calls system($_GET['x']) — spawning a shell subprocess with the attacker's controlled argument.
4
Attacker achieves RCE. From the shell: cat .env | grep DB_PASSWORD, read AWS keys, pivot to database, or deploy a persistent webshell. Every request thereafter can re-execute commands via the injected expression.

Fix commit saleem-hadad/larecipe@c1d0d56 — Strip Blade directives from untrusted markdown
// CHANGE: Never render untrusted markdown as a Blade template. // Instead, convert markdown to HTML and strip any embedded Blade syntax // before rendering. If Blade rendering is required (for legitimate use cases), // use a sandboxed environment with a restricted set of allowed directives. public function render($markdown, $recipe) { // Step 1: Parse markdown to HTML using a safe, sanitized renderer. // This converts markdown syntax (headers, links, code blocks) to HTML // WITHOUT evaluating any Blade template expressions. $html = Markdown::parse($markdown); // Step 2: Strip any Blade template expressions that somehow survived // the markdown-to-HTML conversion. This is a belt-and-suspenders check — // if the markdown renderer accidentally passes through template syntax, // we neutralize it here before it ever reaches Blade::render(). $html = $this->stripBladeExpressions($html); // Step 3: Only then pass to Blade (if you still need Blade features // like component includes). Use a restricted compile callback. // If no Blade features are needed in the final output, return $html directly. return Blade::render($html, ['recipe' => $recipe], true); } private function stripBladeExpressions($content) { // Remove {{ }}, {!! !!}, @{{ }}, and any @directive(...) patterns. // These patterns, if present in HTML output from the markdown parser, // indicate a potential SSTI payload that should be neutralized. return preg_replace( '/(\\{\\{[^{}]*\\}\\}|\\{!![^{}]*!!\\}|@\\w+(\\{[^}]*\\})?)/', '[redacted]', $content ); }

Why this works: The fix separates markdown rendering from template compilation. By converting markdown to HTML before any Blade evaluation, the attacker's template expressions are neutralized. The stripBladeExpressions() regex is a defense-in-depth layer — if the markdown parser ever emits Blade syntax, it gets stripped before reaching the Blade engine. The ideal fix is to never pass untrusted markdown to Blade::render() at all.


🔴 PullLight — Critical Finding (CVSS 10.0)
[CRITICAL] Server-Side Template Injection (SSTI) — LaRecipe.php render() method

Blade::render($markdown, ...) receives untrusted markdown content directly — no sanitization, no stripping of Blade template expressions. An attacker injecting {{ system($_GET['c']) }} into any markdown field achieves pre-auth RCE with a single HTTP request.

This is CWE-1336 ( SSTI ) — a well-established vulnerability class where template engines evaluate attacker-controlled input as code. The fix must separate markdown parsing from template compilation: parse markdown to HTML first, then strip or sandbox any remaining template syntax before Blade evaluation.

CVSS 10.0: AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H — maximum severity across all metrics. No conditions to meet, no user interaction required.
→ Fix: Parse markdown to HTML before Blade rendering. Strip template expressions ({!! !!}, {{ }}, @ directives) from untrusted content. Never pass raw user input to Blade::render(). Fix commit: saleem-hadad/larecipe@c1d0d568.

// root cause
LaRecipe's render() method was designed to support Blade template features in markdown documentation — a legitimate feature for documentation authors who want to use Laravel components inside their docs. However, the implementation passed raw markdown content directly to Blade::render() without separating the markdown parsing step from the template compilation step. This is a classic Server-Side Template Injection vulnerability: the template engine evaluates expressions in user-controlled content as executable code.

The issue is architectural: markdown is a text format meant to be converted to HTML, not a template language. By routing untrusted markdown through a template engine without a sanitization gate, LaRecipe opened every server running it to pre-auth RCE. The fix (parse → strip → render) restores the intended separation of concerns.

// what you can do

Is LaRecipe's documentation site publicly accessible?
In most deployments, yes. LaRecipe is typically used to serve in-app documentation at a route like /docs or /recipes. These pages are often publicly accessible — no auth required to view documentation. If the content is user-submitted (e.g., via a comment or edit form), the attack surface includes unauthenticated attackers.
What's the difference between SSTI and XSS?
XSS (Cross-Site Scripting) runs JavaScript in the victim's browser — it can steal cookies, redirect, or deface pages. SSTI runs code on the server — it gives the attacker a shell on your server. SSTI is categorically more severe because it bypasses the browser sandbox entirely. RCE via SSTI gives the attacker everything: environment variables, database access, file system, and the ability to pivot to other services.
Does the {!! !!} syntax make this worse?
Yes. {{ }} auto-escapes output; {!! !!} writes raw (unescaped) HTML. For an SSTI attacker, raw output is often preferable — it means their template expression output appears directly in the HTML response, which can make exploitation easier. But with the right expressions, {{ }} is sufficient for RCE.
How do I check if I'm running a vulnerable version?
composer show saleem-hadad/larecipe and compare the version against the patch. If you can't upgrade immediately, you can temporarily disable documentation editing by non-admin users or add a WAF rule that blocks {{, {!!, and @ patterns in markdown content fields.

July 2025 CVE-2025-53833 assigned and security advisory GHSA-jv7x-xhv2-p5v2 published. LaRecipe maintainer Saleem Hadad discloses the SSTI vulnerability and releases a patched version.
July 2025 Fix commit saleem-hadad/larecipe@c1d0d56889655ce5f2645db5acf0e78d5fc3b36b lands — markdown parsed to HTML before Blade rendering, Blade expressions stripped from untrusted content.
June 2026 PullLight documents CVE-2025-53833 as case study #28 — demonstrating SSTI detection via template engine analysis in PR code review.

Don't let SSTI slip into your PRs

More CVE Case Studies