mime: reject CR and LF in mail part name and filename

Closes #22247
This commit is contained in:
Alhuda Khan 2026-07-02 16:23:36 +05:30 committed by Daniel Stenberg
parent 02214d98f7
commit c3f9ef13f6
No known key found for this signature in database
GPG key ID: 5CC908FDB71E12C2
5 changed files with 165 additions and 1 deletions

View file

@ -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);

View file

@ -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 \
\

20
tests/data/test3227 Normal file
View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="US-ASCII"?>
<testcase>
<info>
<keywords>
unittest
mime
SMTP
</keywords>
</info>
# Client-side
<client>
<features>
unittest
</features>
<name>
reject CR and LF in mail mime part name and filename
</name>
</client>
</testcase>

View file

@ -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

133
tests/unit/unit3227.c Normal file
View file

@ -0,0 +1,133 @@
/***************************************************************************
* _ _ ____ _
* Project ___| | | | _ \| |
* / __| | | | |_) | |
* | (__| |_| | _ <| |___
* \___|\___/|_| \_\_____|
*
* Copyright (C) Daniel Stenberg, <daniel@haxx.se>, 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