diff --git a/lib/mime.c b/lib/mime.c
index 077e599e43..11eb66207d 100644
--- a/lib/mime.c
+++ b/lib/mime.c
@@ -1708,6 +1708,16 @@ static CURLcode add_content_disposition(struct Curl_easy *data,
CURLcode result = CURLE_OK;
char *name = NULL;
char *filename = NULL;
+ /* The mail (and legacy mime_formescape) strategy quotes the name and
+ filename with a backslash and has no in-band way to represent a CR or
+ LF, so one embedded in the value would split the generated header. The
+ form strategy percent-encodes CR/LF (see escape_string) and is safe. */
+ bool backslash = (strategy == MIMESTRATEGY_MAIL) ||
+ (data && data->set.mime_formescape);
+ if(backslash &&
+ ((part->name && part->name[strcspn(part->name, "\r\n")]) ||
+ (part->filename && part->filename[strcspn(part->filename, "\r\n")])))
+ return CURLE_BAD_FUNCTION_ARGUMENT;
if(part->name) {
name = escape_string(data, part->name, strategy);
diff --git a/tests/data/Makefile.am b/tests/data/Makefile.am
index 53a1927146..e124958874 100644
--- a/tests/data/Makefile.am
+++ b/tests/data/Makefile.am
@@ -284,7 +284,7 @@ test3100 test3101 test3102 test3103 test3104 test3105 test3106 \
test3200 test3201 test3202 test3203 test3204 test3205 test3206 test3207 \
test3208 test3209 test3210 test3211 test3212 test3213 test3214 test3215 \
test3216 test3217 test3218 test3219 test3220 test3221 test3222 \
-test3223 test3224 test3225 test3226 \
+test3223 test3224 test3225 test3226 test3227 \
\
test3300 test3301 test3302 test3303 test3304 test3305 \
\
diff --git a/tests/data/test3227 b/tests/data/test3227
new file mode 100644
index 0000000000..cd9d74b30f
--- /dev/null
+++ b/tests/data/test3227
@@ -0,0 +1,20 @@
+
+
+
+
+unittest
+mime
+SMTP
+
+
+
+# Client-side
+
+
+unittest
+
+
+reject CR and LF in mail mime part name and filename
+
+
+
diff --git a/tests/unit/Makefile.inc b/tests/unit/Makefile.inc
index d1fec804ee..c292e7a198 100644
--- a/tests/unit/Makefile.inc
+++ b/tests/unit/Makefile.inc
@@ -48,4 +48,5 @@ TESTS_C = \
unit2600.c unit2601.c unit2602.c unit2603.c unit2604.c unit2605.c \
unit3200.c unit3205.c \
unit3211.c unit3212.c unit3213.c unit3214.c unit3216.c unit3219.c \
+ unit3227.c \
unit3300.c unit3301.c unit3302.c unit3303.c unit3304.c unit3400.c
diff --git a/tests/unit/unit3227.c b/tests/unit/unit3227.c
new file mode 100644
index 0000000000..42d068e99d
--- /dev/null
+++ b/tests/unit/unit3227.c
@@ -0,0 +1,133 @@
+/***************************************************************************
+ * _ _ ____ _
+ * Project ___| | | | _ \| |
+ * / __| | | | |_) | |
+ * | (__| |_| | _ <| |___
+ * \___|\___/|_| \_\_____|
+ *
+ * Copyright (C) Daniel Stenberg, , et al.
+ *
+ * This software is licensed as described in the file COPYING, which
+ * you should have received as part of this distribution. The terms
+ * are also available at https://curl.se/docs/copyright.html.
+ *
+ * You may opt to use, copy, modify, merge, publish, distribute and/or sell
+ * copies of the Software, and permit persons to whom the Software is
+ * furnished to do so, under the terms of the COPYING file.
+ *
+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+ * KIND, either express or implied.
+ *
+ * SPDX-License-Identifier: curl
+ *
+ ***************************************************************************/
+#include "unitcheck.h"
+#include "urldata.h"
+#include "mime.h"
+
+#if !defined(CURL_DISABLE_MIME) && (!defined(CURL_DISABLE_HTTP) || \
+ !defined(CURL_DISABLE_SMTP) || !defined(CURL_DISABLE_IMAP))
+
+/* does any generated part header carry a raw CR or LF, i.e. a split line? */
+static bool headers_have_ctrl(const curl_mimepart *part)
+{
+ const struct curl_slist *h;
+ for(h = part->curlheaders; h; h = h->next)
+ if(h->data[strcspn(h->data, "\r\n")])
+ return TRUE;
+ return FALSE;
+}
+
+static CURLcode t3227_setup(void)
+{
+ CURLcode result = CURLE_OK;
+ global_init(CURL_GLOBAL_ALL);
+ return result;
+}
+
+static void t3227_stop(CURL *easy, curl_mime *mime)
+{
+ curl_mime_free(mime);
+ if(easy)
+ curl_easy_cleanup(easy);
+ curl_global_cleanup();
+}
+
+static CURLcode test_unit3227(const char *arg)
+{
+ CURL *easy = NULL;
+ curl_mime *mime = NULL;
+ curl_mimepart *part;
+
+ UNITTEST_BEGIN(t3227_setup())
+
+ easy = curl_easy_init();
+ abort_unless(easy, "curl_easy_init()");
+ mime = curl_mime_init(easy);
+ abort_unless(mime, "curl_mime_init()");
+
+ /* A CR or LF in the mail-part name or filename would split the generated
+ Content-Disposition header. The mail strategy must reject it. */
+
+ part = curl_mime_addpart(mime);
+ abort_unless(part, "curl_mime_addpart()");
+ curl_mime_data(part, "x", 1);
+ curl_mime_filename(part, "a\r\nX-Injected: 1");
+ fail_unless(Curl_mime_prepare_headers(easy, part, NULL, NULL,
+ MIMESTRATEGY_MAIL) ==
+ CURLE_BAD_FUNCTION_ARGUMENT,
+ "CRLF filename accepted for mail");
+
+ part = curl_mime_addpart(mime);
+ abort_unless(part, "curl_mime_addpart()");
+ curl_mime_data(part, "x", 1);
+ curl_mime_name(part, "a\nb");
+ fail_unless(Curl_mime_prepare_headers(easy, part, NULL, NULL,
+ MIMESTRATEGY_MAIL) ==
+ CURLE_BAD_FUNCTION_ARGUMENT,
+ "LF name accepted for mail");
+
+ part = curl_mime_addpart(mime);
+ abort_unless(part, "curl_mime_addpart()");
+ curl_mime_data(part, "x", 1);
+ curl_mime_filename(part, "a\rb");
+ fail_unless(Curl_mime_prepare_headers(easy, part, NULL, NULL,
+ MIMESTRATEGY_MAIL) ==
+ CURLE_BAD_FUNCTION_ARGUMENT,
+ "CR filename accepted for mail");
+
+ /* A regular name and filename are accepted and produce a clean header. */
+ part = curl_mime_addpart(mime);
+ abort_unless(part, "curl_mime_addpart()");
+ curl_mime_data(part, "x", 1);
+ curl_mime_name(part, "field");
+ curl_mime_filename(part, "readme.txt");
+ fail_unless(Curl_mime_prepare_headers(easy, part, NULL, NULL,
+ MIMESTRATEGY_MAIL) == CURLE_OK,
+ "plain mail part rejected");
+ fail_if(headers_have_ctrl(part), "clean mail header split");
+
+ /* The HTTP form strategy percent-encodes CR/LF, so it stays accepted and
+ produces no split line. */
+ part = curl_mime_addpart(mime);
+ abort_unless(part, "curl_mime_addpart()");
+ curl_mime_data(part, "x", 1);
+ curl_mime_filename(part, "a\r\nX-Injected: 1");
+ fail_unless(Curl_mime_prepare_headers(easy, part, NULL, NULL,
+ MIMESTRATEGY_FORM) == CURLE_OK,
+ "CRLF filename rejected for form");
+ fail_if(headers_have_ctrl(part), "form header split despite encoding");
+
+ UNITTEST_END(t3227_stop(easy, mime))
+}
+
+#else /* mime disabled */
+
+static CURLcode test_unit3227(const char *arg)
+{
+ UNITTEST_BEGIN_SIMPLE
+ puts("nothing to do when mime is disabled");
+ UNITTEST_END_SIMPLE
+}
+
+#endif