Skip to content

Commit c55494a

Browse files
authored
[CVE-2026-54721] Prevent RCE via email recipient subject (#1442)
1 parent 23bc7f1 commit c55494a

2 files changed

Lines changed: 32 additions & 1 deletion

File tree

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
],
3232
"require": {
3333
"php": "^8.1",
34-
"silverstripe/framework": "^5.4",
34+
"silverstripe/framework": "^5.4.30",
3535
"silverstripe/cms": "^5",
3636
"symbiote/silverstripe-gridfieldextensions": "^4",
3737
"silverstripe/segment-field": "^3",

tests/php/Control/UserDefinedFormControllerTest.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,37 @@ public function testRecipientSubjectMergeFields()
402402
$this->assertEmailSent('test@example.com', 'no-reply@example.com', 'Email Subject: Basic Value');
403403
}
404404

405+
/**
406+
* The EmailSubject is rendered as a template. An author controlled subject must never have PHP
407+
* within it evaluated (which previously allowed remote code execution via the <%t %> default
408+
* string handling). The payload below must be treated as inert text.
409+
*/
410+
public function testRecipientSubjectDoesNotEvaluatePhp()
411+
{
412+
$form = $this->setupFormFrontend();
413+
414+
$rceFile = ASSETS_PATH . '/userforms_rce_test.txt';
415+
if (file_exists($rceFile ?? '')) {
416+
unlink($rceFile ?? '');
417+
}
418+
419+
$recipient = $this->objFromFixture(EmailRecipient::class, 'recipient-1');
420+
$recipient->EmailSubject = '<%t Foo "{${\'file_put_contents\'(\''
421+
. $rceFile . '\',\'pwned\')}}" %>';
422+
$recipient->write();
423+
424+
$this->autoFollowRedirection = false;
425+
$this->clearEmails();
426+
427+
$this->get($form->URLSegment);
428+
429+
$field = $this->objFromFixture(EditableTextField::class, 'basic-text');
430+
$this->submitForm('UserForm_Form_' . $form->ID, null, [$field->Name => 'Basic Value']);
431+
432+
// The payload must not have been executed, so no file should have been written.
433+
$this->assertFileDoesNotExist($rceFile);
434+
}
435+
405436
public function testImageThumbnailCreated()
406437
{
407438
Config::modify()->set(Upload_Validator::class, 'use_is_uploaded_file', false);

0 commit comments

Comments
 (0)