← 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.
TL;DR
// 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.
CVSS v3.1 Vector
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
The Bug — Vulnerable Code
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;
}
Exploit Chain
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.
The Fix — After (c1d0d56889655ce5f2645db5acf0e78d5fc3b36b)
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.
// 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
- Upgrade LaRecipe → patched version containing fix commit
c1d0d56889655
- Audit your documentation routes → check for any
Blade::render() calls receiving user-supplied content
- Restrict documentation edits → limit who can submit or edit markdown content in your LaRecipe installation
- Scan for Blade expressions → search your
resources/docs/ directory for {{, {!!, or @ patterns
- Install PullLight → catches SSTI and template injection vulnerabilities in PRs before they merge
FAQ
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.
Timeline
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
Fix & Resources
Fix commit: c1d0d56889655ce5f2645db5acf0e78d5fc3b36b — markdown-to-HTML parse before Blade rendering
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.
saleem-hadad/larecipe@c1d0d568.