diff --git a/resources/views/forms/automagic-email.antlers.html b/resources/views/forms/automagic-email.antlers.html
index cab5234e64e..d9255357c3c 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 ?? value) | sanitize }}
{{ elseif fieldtype == "radio" }}
- {{ value:label ?? value }}
+ {{ (value:label ?? value) | sanitize }}
{{ elseif fieldtype == "select" || fieldtype == "checkboxes" || fieldtype == "dictionary" }}
- {{ value }}{{ label ?? value }}{{ if !last }}, {{ /if }}{{ /value }}
+ {{ value }}{{ (label ?? 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' => '">