From 217bec20f171b18144cfe713a69c0153d0dc550c Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Wed, 8 Jul 2026 13:51:03 -0400 Subject: [PATCH 1/2] wip --- .../views/forms/automagic-email.antlers.html | 10 +-- tests/Forms/EmailTest.php | 64 +++++++++++++++++++ 2 files changed, 69 insertions(+), 5 deletions(-) diff --git a/resources/views/forms/automagic-email.antlers.html b/resources/views/forms/automagic-email.antlers.html index cab5234e64e..7fa1380f860 100644 --- a/resources/views/forms/automagic-email.antlers.html +++ b/resources/views/forms/automagic-email.antlers.html @@ -7,19 +7,19 @@ {{ value }}{{ permalink }}{{ if !last }}, {{ /if }}{{ /value }} {{ elseif fieldtype == "select" && !config:multiple }} - {{ value:label ?? value }} + {{ value:label | sanitize ?? value | sanitize }} {{ elseif fieldtype == "radio" }} - {{ value:label ?? value }} + {{ value:label | sanitize ?? value | sanitize }} {{ elseif fieldtype == "select" || fieldtype == "checkboxes" || fieldtype == "dictionary" }} - {{ value }}{{ label ?? value }}{{ if !last }}, {{ /if }}{{ /value }} + {{ value }}{{ label | sanitize ?? value | sanitize }}{{ if !last }}, {{ /if }}{{ /value }} {{ elseif value|is_iterable }} - {{ value | json }} + {{ value | json | sanitize }} {{ else }} - {{ value | nl2br }} + {{ value | sanitize | nl2br }} {{ /if }} {{ /if }} diff --git a/tests/Forms/EmailTest.php b/tests/Forms/EmailTest.php index 75a81d9cadc..6d325f5b962 100644 --- a/tests/Forms/EmailTest.php +++ b/tests/Forms/EmailTest.php @@ -167,6 +167,70 @@ public function it_adds_data_to_the_view() $this->assertEquals('Statamic', (string) $email->viewData['company']['company_name']); } + #[Test] + public function it_escapes_submitted_values_in_the_automagic_email() + { + $formBlueprint = Blueprint::makeFromFields([ + 'name' => ['type' => 'text'], + 'message' => ['type' => 'textarea'], + + // The select/radio/checkboxes branches emit `label ?? value`, and the label + // falls back to the raw value when there's no matching option. The raw value + // is attacker-controlled, so it must be escaped. The option label is author + // controlled (it lives in the blueprint), so it's not really exploitable, but + // we escape it too for consistency. Both situations are asserted below. + 'select_labelled' => ['type' => 'select', 'options' => ['a' => '']], + 'select_raw' => ['type' => 'select'], + 'radio_labelled' => ['type' => 'radio', 'options' => ['b' => '']], + 'radio_raw' => ['type' => 'radio'], + 'checkboxes' => ['type' => 'checkboxes', 'options' => ['c' => '']], + ]); + + BlueprintRepository::shouldReceive('find')->with('forms.test')->andReturn($formBlueprint); + + $form = tap(Form::make('test'))->save(); + + $submission = $form->makeSubmission()->data([ + 'name' => '', + 'message' => "line one\n", + 'select_labelled' => 'a', + 'select_raw' => '">', + 'radio_labelled' => 'b', + 'radio_raw' => '">', + 'checkboxes' => ['c', 'raw-checkbox'], + ]); + + $email = new Email($submission, ['to' => 'test@test.com'], Site::default()); + + $body = $email->render(); + + // Attacker-controlled values are escaped, not emitted as live markup. + $assertEscaped = function ($raw, $escaped) use ($body) { + $this->assertStringNotContainsString($raw, $body); + $this->assertStringContainsString($escaped, $body); + }; + + // text / textarea + $assertEscaped('', '<img src=x onerror=alert(1)>'); + $assertEscaped('', '<script>alert(2)</script>'); + + // select — the author-controlled option label (sanitized for consistency) and + // the attacker-controlled raw fallback value. + $assertEscaped('', '<script>select-label</script>'); + $assertEscaped('', '<svg onload=alert(3)>'); + + // radio — same, option label and raw fallback value. + $assertEscaped('', '<script>radio-label</script>'); + $assertEscaped('', '<svg onload=alert(4)>'); + + // checkboxes — same, option label and raw fallback value. + $assertEscaped('', '<script>checkbox-label</script>'); + $assertEscaped('raw-checkbox', '<b>raw-checkbox</b>'); + + // Legitimate multiline text still renders line breaks. + $this->assertStringContainsString('line one Date: Wed, 8 Jul 2026 13:53:48 -0400 Subject: [PATCH 2/2] combine --- resources/views/forms/automagic-email.antlers.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/views/forms/automagic-email.antlers.html b/resources/views/forms/automagic-email.antlers.html index 7fa1380f860..d9255357c3c 100644 --- a/resources/views/forms/automagic-email.antlers.html +++ b/resources/views/forms/automagic-email.antlers.html @@ -7,13 +7,13 @@ {{ value }}{{ permalink }}{{ if !last }}, {{ /if }}{{ /value }} {{ elseif fieldtype == "select" && !config:multiple }} - {{ value:label | sanitize ?? value | sanitize }} + {{ (value:label ?? value) | sanitize }} {{ elseif fieldtype == "radio" }} - {{ value:label | sanitize ?? value | sanitize }} + {{ (value:label ?? value) | sanitize }} {{ elseif fieldtype == "select" || fieldtype == "checkboxes" || fieldtype == "dictionary" }} - {{ value }}{{ label | sanitize ?? value | sanitize }}{{ if !last }}, {{ /if }}{{ /value }} + {{ value }}{{ (label ?? value) | sanitize }}{{ if !last }}, {{ /if }}{{ /value }} {{ elseif value|is_iterable }} {{ value | json | sanitize }}