1
0
mirror of https://github.com/moparisthebest/curl synced 2024-12-22 08:08:50 -05:00

formpost: support quotes, commas and semicolon in file names

- document the double-quote and backslash need be escaped if quoting.
- libcurl formdata escape double-quote in filename by backslash.
- curl formparse can parse filename both contains '"' and ',' or ';'.
- curl now can uploading file with ',' or ';' in filename.

Bug: http://curl.haxx.se/bug/view.cgi?id=1171
This commit is contained in:
Ulion 2013-01-21 23:20:09 +01:00 committed by Daniel Stenberg
parent fa176376c8
commit 2698520aef
7 changed files with 316 additions and 113 deletions

View File

@ -484,6 +484,17 @@ filename=, like this:
\fBcurl\fP -F "file=@localfile;filename=nameinpost" url.com \fBcurl\fP -F "file=@localfile;filename=nameinpost" url.com
If filename/path contains ',' or ';', it must be quoted by double-quotes like:
\fBcurl\fP -F "file=@\\"localfile\\";filename=\\"nameinpost\\"" url.com
or
\fBcurl\fP -F 'file=@"localfile";filename="nameinpost"' url.com
Note that if a filename/path is quoted by double-quotes, any double-quote
or backslash within the filename must be escaped by backslash.
See further examples and details in the MANUAL. See further examples and details in the MANUAL.
This option can be used multiple times. This option can be used multiple times.

View File

@ -1025,6 +1025,47 @@ static char *strippath(const char *fullfile)
return base; /* returns an allocated string or NULL ! */ return base; /* returns an allocated string or NULL ! */
} }
static CURLcode formdata_add_filename(const struct curl_httppost *file,
struct FormData **form,
curl_off_t *size)
{
CURLcode result = CURLE_OK;
char *filename = file->showfilename;
char *filebasename = NULL;
char *filename_escaped = NULL;
if(!filename) {
filebasename = strippath(file->contents);
if(!filebasename)
return CURLE_OUT_OF_MEMORY;
filename = filebasename;
}
if(strchr(filename, '\\') || strchr(filename, '"')) {
char *p0, *p1;
/* filename need be escaped */
filename_escaped = malloc(strlen(filename)*2+1);
if(!filename_escaped)
return CURLE_OUT_OF_MEMORY;
p0 = filename_escaped;
p1 = filename;
while(*p1) {
if(*p1 == '\\' || *p1 == '"')
*p0++ = '\\';
*p0++ = *p1++;
}
*p0 = '\0';
filename = filename_escaped;
}
result = AddFormDataf(form, size,
"; filename=\"%s\"",
filename);
Curl_safefree(filename_escaped);
Curl_safefree(filebasename);
return result;
}
/* /*
* Curl_getformdata() converts a linked list of "meta data" into a complete * Curl_getformdata() converts a linked list of "meta data" into a complete
* (possibly huge) multipart formdata. The input list is in 'post', while the * (possibly huge) multipart formdata. The input list is in 'post', while the
@ -1139,22 +1180,13 @@ CURLcode Curl_getformdata(struct SessionHandle *data,
if(post->more) { if(post->more) {
/* if multiple-file */ /* if multiple-file */
char *filebasename = NULL;
if(!file->showfilename) {
filebasename = strippath(file->contents);
if(!filebasename) {
result = CURLE_OUT_OF_MEMORY;
break;
}
}
result = AddFormDataf(&form, &size, result = AddFormDataf(&form, &size,
"\r\n--%s\r\nContent-Disposition: " "\r\n--%s\r\nContent-Disposition: "
"attachment; filename=\"%s\"", "attachment",
fileboundary, fileboundary);
(file->showfilename?file->showfilename: if(result)
filebasename)); break;
Curl_safefree(filebasename); result = formdata_add_filename(file, &form, &size);
if(result) if(result)
break; break;
} }
@ -1164,14 +1196,7 @@ CURLcode Curl_getformdata(struct SessionHandle *data,
HTTPPOST_CALLBACK cases the ->showfilename struct member is always HTTPPOST_CALLBACK cases the ->showfilename struct member is always
assigned at this point */ assigned at this point */
if(post->showfilename || (post->flags & HTTPPOST_FILENAME)) { if(post->showfilename || (post->flags & HTTPPOST_FILENAME)) {
char *filebasename= result = formdata_add_filename(post, &form, &size);
(!post->showfilename)?strippath(post->contents):NULL;
result = AddFormDataf(&form, &size,
"; filename=\"%s\"",
(post->showfilename?post->showfilename:
filebasename));
Curl_safefree(filebasename);
} }
if(result) if(result)

View File

@ -5,7 +5,7 @@
* | (__| |_| | _ <| |___ * | (__| |_| | _ <| |___
* \___|\___/|_| \_\_____| * \___|\___/|_| \_\_____|
* *
* Copyright (C) 1998 - 2012, Daniel Stenberg, <daniel@haxx.se>, et al. * Copyright (C) 1998 - 2013, Daniel Stenberg, <daniel@haxx.se>, et al.
* *
* This software is licensed as described in the file COPYING, which * This software is licensed as described in the file COPYING, which
* you should have received as part of this distribution. The terms * you should have received as part of this distribution. The terms
@ -34,13 +34,73 @@
#include "memdebug.h" /* keep this as LAST include */ #include "memdebug.h" /* keep this as LAST include */
/*
* helper function to get a word from form param
* after call get_parm_word, str either point to string end
* or point to any of end chars.
*/
static char *get_param_word(char **str, char **end_pos)
{
char *ptr = *str;
char *word_begin = NULL;
char *ptr2;
char *escape = NULL;
const char *end_chars = ";,";
/* the first non-space char is here */
word_begin = ptr;
if(*ptr == '"') {
++ptr;
while(*ptr) {
if(*ptr == '\\') {
if(ptr[1] == '\\' || ptr[1] == '"') {
/* remember the first escape position */
if(!escape)
escape = ptr;
/* skip escape of back-slash or double-quote */
ptr += 2;
continue;
}
}
if(*ptr == '"') {
*end_pos = ptr;
if(escape) {
/* has escape, we restore the unescaped string here */
ptr = ptr2 = escape;
do {
if(*ptr == '\\' && (ptr[1] == '\\' || ptr[1] == '"'))
++ptr;
*ptr2++ = *ptr++;
}
while(ptr < *end_pos);
*end_pos = ptr2;
}
while(*ptr && NULL==strchr(end_chars, *ptr))
++ptr;
*str = ptr;
return word_begin+1;
}
++ptr;
}
/* end quote is missing, treat it as non-quoted. */
ptr = word_begin;
}
while(*ptr && NULL==strchr(end_chars, *ptr))
++ptr;
*str = *end_pos = ptr;
return word_begin;
}
/*************************************************************************** /***************************************************************************
* *
* formparse() * formparse()
* *
* Reads a 'name=value' parameter and builds the appropriate linked list. * Reads a 'name=value' parameter and builds the appropriate linked list.
* *
* Specify files to upload with 'name=@filename'. Supports specified * Specify files to upload with 'name=@filename', or 'name=@"filename"'
* in case the filename contain ',' or ';'. Supports specified
* given Content-Type of the files. Such as ';type=<content-type>'. * given Content-Type of the files. Such as ';type=<content-type>'.
* *
* If literal_value is set, any initial '@' or '<' in the value string * If literal_value is set, any initial '@' or '<' in the value string
@ -51,6 +111,10 @@
* *
* 'name=@filename,filename2,filename3' * 'name=@filename,filename2,filename3'
* *
* or use double-quotes quote the filename:
*
* 'name=@"filename","filename2","filename3"'
*
* If you want content-types specified for each too, write them like: * If you want content-types specified for each too, write them like:
* *
* 'name=@filename;type=image/gif,filename2,filename3' * 'name=@filename;type=image/gif,filename2,filename3'
@ -64,7 +128,12 @@
* To upload a file, but to fake the file name that will be included in the * To upload a file, but to fake the file name that will be included in the
* formpost, do like this: * formpost, do like this:
* *
* 'name=@filename;filename=/dev/null' * 'name=@filename;filename=/dev/null' or quote the faked filename like:
* 'name=@filename;filename="play, play, and play.txt"'
*
* If filename/path contains ',' or ';', it must be quoted by double-quotes,
* else curl will fail to figure out the correct filename. if the filename
* tobe quoted contains '"' or '\', '"' and '\' must be escaped by backslash.
* *
* This function uses curl_formadd to fulfill it's job. Is heavily based on * This function uses curl_formadd to fulfill it's job. Is heavily based on
* the old curl_formparse code. * the old curl_formparse code.
@ -86,7 +155,6 @@ int formparse(struct Configurable *config,
char *contp; char *contp;
const char *type = NULL; const char *type = NULL;
char *sep; char *sep;
char *sep2;
if((1 == sscanf(input, "%255[^=]=", name)) && if((1 == sscanf(input, "%255[^=]=", name)) &&
((contp = strchr(input, '=')) != NULL)) { ((contp = strchr(input, '=')) != NULL)) {
@ -107,38 +175,29 @@ int formparse(struct Configurable *config,
struct multi_files *multi_start = NULL; struct multi_files *multi_start = NULL;
struct multi_files *multi_current = NULL; struct multi_files *multi_current = NULL;
contp++; char *ptr = contp;
char *end = ptr + strlen(ptr);
do { do {
/* since this was a file, it may have a content-type specifier /* since this was a file, it may have a content-type specifier
at the end too, or a filename. Or both. */ at the end too, or a filename. Or both. */
char *ptr;
char *filename = NULL; char *filename = NULL;
char *word_end;
sep = strchr(contp, ';'); bool semicolon;
sep2 = strchr(contp, ',');
/* pick the closest */
if(sep2 && (sep2 < sep)) {
sep = sep2;
/* no type was specified! */
}
type = NULL; type = NULL;
if(sep) { ++ptr;
bool semicolon = (';' == *sep) ? TRUE : FALSE; contp = get_param_word(&ptr, &word_end);
semicolon = (';' == *ptr) ? TRUE : FALSE;
*word_end = '\0'; /* terminate the contp */
*sep = '\0'; /* terminate file name at separator */ /* have other content, continue parse */
while(semicolon) {
ptr = sep+1; /* point to the text following the separator */ /* have type or filename field */
++ptr;
while(semicolon && ptr && (','!= *ptr)) { while(*ptr && (ISSPACE(*ptr)))
++ptr;
/* pass all white spaces */
while(ISSPACE(*ptr))
ptr++;
if(checkprefix("type=", ptr)) { if(checkprefix("type=", ptr)) {
/* set type pointer */ /* set type pointer */
@ -159,66 +218,61 @@ int formparse(struct Configurable *config,
/* there's a semicolon following - we check if it is a filename /* there's a semicolon following - we check if it is a filename
specified and if not we simply assume that it is text that specified and if not we simply assume that it is text that
the user wants included in the type and include that too up the user wants included in the type and include that too up
to the next zero or semicolon. */ to the next sep. */
ptr = sep;
if(*sep==';') { if(*sep==';') {
if(!checkprefix(";filename=", sep)) { if(!checkprefix(";filename=", sep)) {
sep2 = strchr(sep+1, ';'); ptr = sep + 1;
if(sep2) (void)get_param_word(&ptr, &sep);
sep = sep2; semicolon = (';' == *ptr) ? TRUE : FALSE;
else
sep = sep + strlen(sep); /* point to end of string */
} }
} }
else else
semicolon = FALSE; semicolon = FALSE;
if(*sep) { if(*sep)
*sep = '\0'; /* zero terminate type string */ *sep = '\0'; /* zero terminate type string */
ptr = sep+1;
}
else
ptr = NULL; /* end */
} }
else if(checkprefix("filename=", ptr)) { else if(checkprefix("filename=", ptr)) {
filename = &ptr[9]; ptr += 9;
ptr = strchr(filename, ';'); filename = get_param_word(&ptr, &word_end);
if(!ptr) { semicolon = (';' == *ptr) ? TRUE : FALSE;
ptr = strchr(filename, ','); *word_end = '\0';
} }
if(ptr) { else {
*ptr = '\0'; /* zero terminate */ /* unknown prefix, skip to next block */
ptr++; char *unknown = NULL;
unknown = get_param_word(&ptr, &word_end);
semicolon = (';' == *ptr) ? TRUE : FALSE;
if(*unknown) {
*word_end = '\0';
warnf(config, "skip unknown form field: %s\n", unknown);
} }
} }
else
/* confusion, bail out of loop */
break;
} }
/* now ptr point to comma or string end */
sep = ptr;
}
/* if type == NULL curl_formadd takes care of the problem */ /* if type == NULL curl_formadd takes care of the problem */
if(!AddMultiFiles(contp, type, filename, &multi_start, if(*contp && !AddMultiFiles(contp, type, filename, &multi_start,
&multi_current)) { &multi_current)) {
warnf(config, "Error building form post!\n"); warnf(config, "Error building form post!\n");
Curl_safefree(contents); Curl_safefree(contents);
FreeMultiInfo(&multi_start, &multi_current); FreeMultiInfo(&multi_start, &multi_current);
return 3; return 3;
} }
contp = sep; /* move the contents pointer to after the separator */
} while(sep && *sep); /* loop if there's another file name */ /* *ptr could be '\0', so we just check with the string end */
} while(ptr < end); /* loop if there's another file name */
/* now we add the multiple files section */ /* now we add the multiple files section */
if(multi_start) { if(multi_start) {
struct curl_forms *forms = NULL; struct curl_forms *forms = NULL;
struct multi_files *ptr = multi_start; struct multi_files *start = multi_start;
unsigned int i, count = 0; unsigned int i, count = 0;
while(ptr) { while(start) {
ptr = ptr->next; start = start->next;
++count; ++count;
} }
forms = malloc((count+1)*sizeof(struct curl_forms)); forms = malloc((count+1)*sizeof(struct curl_forms));
@ -228,9 +282,9 @@ int formparse(struct Configurable *config,
FreeMultiInfo(&multi_start, &multi_current); FreeMultiInfo(&multi_start, &multi_current);
return 4; return 4;
} }
for(i = 0, ptr = multi_start; i < count; ++i, ptr = ptr->next) { for(i = 0, start = multi_start; i < count; ++i, start = start->next) {
forms[i].option = ptr->form.option; forms[i].option = start->form.option;
forms[i].value = ptr->form.value; forms[i].value = start->form.value;
} }
forms[count].option = CURLFORM_END; forms[count].option = CURLFORM_END;
FreeMultiInfo(&multi_start, &multi_current); FreeMultiInfo(&multi_start, &multi_current);

View File

@ -75,7 +75,7 @@ test1094 test1095 test1096 test1097 test1098 test1099 test1100 test1101 \
test1102 test1103 test1104 test1105 test1106 test1107 test1108 test1109 \ test1102 test1103 test1104 test1105 test1106 test1107 test1108 test1109 \
test1110 test1111 test1112 test1113 test1114 test1115 test1116 test1117 \ test1110 test1111 test1112 test1113 test1114 test1115 test1116 test1117 \
test1118 test1119 test1120 test1121 test1122 test1123 test1124 test1125 \ test1118 test1119 test1120 test1121 test1122 test1123 test1124 test1125 \
test1126 test1127 test1128 test1129 test1130 test1131 test1132 \ test1126 test1127 test1128 test1129 test1130 test1131 test1132 test1133 \
test1200 test1201 test1202 test1203 test1204 test1205 test1206 test1207 \ test1200 test1201 test1202 test1203 test1204 test1205 test1206 test1207 \
test1208 test1209 test1210 test1211 \ test1208 test1209 test1210 test1211 \
test1220 test1221 test1222 test1223 \ test1220 test1221 test1222 test1223 \

95
tests/data/test1133 Normal file
View File

@ -0,0 +1,95 @@
<testcase>
<info>
<keywords>
HTTP
HTTP FORMPOST
</keywords>
</info>
# Server-side
<reply>
<data>
HTTP/1.1 200 OK
Date: Thu, 09 Nov 2010 14:49:00 GMT
Server: test-server/fake
Content-Length: 10
blablabla
</data>
</reply>
# Client-side
<client>
<server>
http
</server>
<name>
HTTP RFC1867-type formposting with filename contains ',', ';', '"'
</name>
<command>
http://%HOSTIP:%HTTPPORT/we/want/1133 -F "file=@\"log/test1133,a\\\"nd;.txt\";type=mo/foo;filename=\"faker,and;.txt\"" -F 'file2=@"log/test1133,a\"nd;.txt"' -F 'file3=@"log/test1133,a\"nd;.txt";type=m/f,"log/test1133,a\"nd;.txt"'
</command>
# We create this file before the command is invoked!
<file name=log/test1133,a"nd;.txt>
foo bar
This is a bar foo
bar
foo
</file>
</client>
# Verify data after the test has been "shot"
<verify>
<strip>
^(User-Agent:|Content-Type: multipart/form-data;|Content-Type: multipart/mixed, boundary=|-------).*
</strip>
<protocol>
POST /we/want/1133 HTTP/1.1
User-Agent: curl/7.10.4 (i686-pc-linux-gnu) libcurl/7.10.4 OpenSSL/0.9.7a ipv6 zlib/1.1.3
Host: %HOSTIP:%HTTPPORT
Accept: */*
Content-Length: 967
Expect: 100-continue
Content-Type: multipart/form-data; boundary=----------------------------24e78000bd32
------------------------------24e78000bd32
Content-Disposition: form-data; name="file"; filename="faker,and;.txt"
Content-Type: mo/foo
foo bar
This is a bar foo
bar
foo
------------------------------24e78000bd32
Content-Disposition: form-data; name="file2"; filename="test1133,a\"nd;.txt"
Content-Type: text/plain
foo bar
This is a bar foo
bar
foo
------------------------------24e78000bd32
Content-Disposition: form-data; name="file3"
Content-Type: multipart/mixed, boundary=----------------------------7f0e85a48b0b
Content-Disposition: attachment; filename="test1133,a\"nd;.txt"
Content-Type: m/f
foo bar
This is a bar foo
bar
foo
Content-Disposition: attachment; filename="test1133,a\"nd;.txt"
Content-Type: text/plain
foo bar
This is a bar foo
bar
foo
------------------------------24e78000bd32--
</protocol>
</verify>
</testcase>

View File

@ -26,7 +26,7 @@ http
HTTP RFC1867-type formposting with filename= and type= HTTP RFC1867-type formposting with filename= and type=
</name> </name>
<command> <command>
http://%HOSTIP:%HTTPPORT/we/want/39 -F name=daniel -F tool=curl --form-string "str1=@literal" --form-string "str2=<verbatim;type=xxx/yyy" -F "file=@log/test39.txt;type=moo/foobar;filename=fakerfile" -F file2=@log/test39.txt http://%HOSTIP:%HTTPPORT/we/want/39 -F name=daniel -F tool=curl --form-string "str1=@literal" --form-string "str2=<verbatim;type=xxx/yyy" -F "file=@log/test39.txt;type=moo/foobar;filename=fakerfile" -F file2=@log/test39.txt -F "file3=@\"log/test39.txt\";type=mo/foo;filename=\"f\\\\\\\\ak\\\\\\er,\\\\an\\d;.t\\\"xt\"" -F 'file4=@"log/test39.txt"; filename="A\\AA\"\"\\\"ZZZ"'
</command> </command>
# We create this file before the command is invoked! # We create this file before the command is invoked!
<file name="log/test39.txt"> <file name="log/test39.txt">
@ -47,7 +47,7 @@ POST /we/want/39 HTTP/1.1
User-Agent: curl/7.10.4 (i686-pc-linux-gnu) libcurl/7.10.4 OpenSSL/0.9.7a ipv6 zlib/1.1.3 User-Agent: curl/7.10.4 (i686-pc-linux-gnu) libcurl/7.10.4 OpenSSL/0.9.7a ipv6 zlib/1.1.3
Host: %HOSTIP:%HTTPPORT Host: %HOSTIP:%HTTPPORT
Accept: */* Accept: */*
Content-Length: 810 Content-Length: 1184
Expect: 100-continue Expect: 100-continue
Content-Type: multipart/form-data; boundary=----------------------------24e78000bd32 Content-Type: multipart/form-data; boundary=----------------------------24e78000bd32
@ -85,6 +85,24 @@ This is a bar foo
bar bar
foo foo
------------------------------24e78000bd32
Content-Disposition: form-data; name="file3"; filename="f\\\\ak\\\\er,\\an\\d;.t\"xt"
Content-Type: mo/foo
foo bar
This is a bar foo
bar
foo
------------------------------24e78000bd32
Content-Disposition: form-data; name="file4"; filename="A\\AA\"\"\\\"ZZZ"
Content-Type: text/plain
foo bar
This is a bar foo
bar
foo
------------------------------24e78000bd32-- ------------------------------24e78000bd32--
</protocol> </protocol>
</verify> </verify>

View File

@ -56,7 +56,7 @@ sub getpartattr {
$inside++; $inside++;
my $attr=$1; my $attr=$1;
while($attr =~ s/ *([^=]*)= *(\"([^\"]*)\"|([^\"> ]*))//) { while($attr =~ s/ *([^=]*)= *(\"([^\"]*)\"|([^\> ]*))//) {
my ($var, $cont)=($1, $2); my ($var, $cont)=($1, $2);
$cont =~ s/^\"(.*)\"$/$1/; $cont =~ s/^\"(.*)\"$/$1/;
$hash{$var}=$cont; $hash{$var}=$cont;