Skip to content

Commit a46b9fa

Browse files
committed
Accept MIME linear whitespace around filename= parameter in Content-Disposition
Tabs after ';' and optional SP/HT around '=' are valid per MIME and are delivered by RESTEasy as-is; the previous parser only skipped literal spaces, so those variants bypassed server.request.body.filenames detection.
1 parent 4f53294 commit a46b9fa

2 files changed

Lines changed: 57 additions & 18 deletions

File tree

dd-java-agent/instrumentation/resteasy/resteasy-appsec-3.0/src/main/java/datadog/trace/instrumentation/resteasy/MultipartHelper.java

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,12 @@ public static List<String> collectFilenames(MultipartFormDataInput ret) {
5151
return filenames;
5252
}
5353

54-
// Quote-aware: semicolons inside quoted filenames (e.g. filename="a;b.php") are not separators
54+
// Quote-aware: semicolons inside quoted filenames (e.g. filename="a;b.php") are not separators.
55+
// Outer loop: i advances to each ';' (skipping quoted strings to avoid treating their contents
56+
// as delimiters), then past MIME linear whitespace (SP/HT) to the start of the parameter name.
57+
// j is a lookahead used only to find '=' after optional whitespace without committing i until
58+
// the parameter is confirmed to be "filename"; this avoids confusing "filename*" (RFC 5987) or
59+
// other "filename"-prefixed parameter names with the plain "filename" parameter.
5560
public static String filenameFromContentDisposition(String cd) {
5661
if (cd == null) return null;
5762
int i = 0;
@@ -69,24 +74,29 @@ public static String filenameFromContentDisposition(String cd) {
6974
}
7075
if (i >= len) break;
7176
i++;
72-
while (i < len && cd.charAt(i) == ' ') i++;
73-
if (cd.regionMatches(true, i, "filename=", 0, 9)) {
74-
i += 9;
75-
if (i >= len) return null;
76-
if (cd.charAt(i) == '"') {
77-
i++;
78-
StringBuilder sb = new StringBuilder();
79-
while (i < len && cd.charAt(i) != '"') {
80-
if (cd.charAt(i) == '\\' && i + 1 < len) i++; // unescape
81-
sb.append(cd.charAt(i++));
77+
while (i < len && (cd.charAt(i) == ' ' || cd.charAt(i) == '\t')) i++;
78+
if (cd.regionMatches(true, i, "filename", 0, 8)) {
79+
int j = i + 8;
80+
while (j < len && (cd.charAt(j) == ' ' || cd.charAt(j) == '\t')) j++;
81+
if (j < len && cd.charAt(j) == '=') {
82+
i = j + 1;
83+
while (i < len && (cd.charAt(i) == ' ' || cd.charAt(i) == '\t')) i++;
84+
if (i >= len) return null;
85+
if (cd.charAt(i) == '"') {
86+
i++;
87+
StringBuilder sb = new StringBuilder();
88+
while (i < len && cd.charAt(i) != '"') {
89+
if (cd.charAt(i) == '\\' && i + 1 < len) i++; // unescape
90+
sb.append(cd.charAt(i++));
91+
}
92+
String name = sb.toString();
93+
return name.isEmpty() ? null : name;
94+
} else {
95+
int start = i;
96+
while (i < len && cd.charAt(i) != ';') i++;
97+
String name = cd.substring(start, i).trim();
98+
return name.isEmpty() ? null : name;
8299
}
83-
String name = sb.toString();
84-
return name.isEmpty() ? null : name;
85-
} else {
86-
int start = i;
87-
while (i < len && cd.charAt(i) != ';') i++;
88-
String name = cd.substring(start, i).trim();
89-
return name.isEmpty() ? null : name;
90100
}
91101
}
92102
}

dd-java-agent/instrumentation/resteasy/resteasy-appsec-3.0/src/test/groovy/MultipartHelperTest.groovy

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,33 @@ class MultipartHelperTest extends Specification {
7070
'form-data; fileName="report.php"',
7171
]
7272
}
73+
74+
def "handles MIME linear whitespace (tab) after semicolon"() {
75+
expect:
76+
MultipartHelper.filenameFromContentDisposition(cd) == expected
77+
78+
where:
79+
cd | expected
80+
'form-data; name="f";\tfilename="evil.php"' | 'evil.php'
81+
'form-data;\tfilename="evil.php"' | 'evil.php'
82+
'form-data; name="f";\t\tfilename="evil.php"' | 'evil.php'
83+
}
84+
85+
def "handles optional whitespace around the equals sign"() {
86+
expect:
87+
MultipartHelper.filenameFromContentDisposition(cd) == expected
88+
89+
where:
90+
cd | expected
91+
'form-data; filename ="report.php"' | 'report.php'
92+
'form-data; filename= "report.php"' | 'report.php'
93+
'form-data; filename = "report.php"' | 'report.php'
94+
'form-data; filename\t=\t"report.php"' | 'report.php'
95+
'form-data; name="f";\tfilename\t=\t"evil.php"' | 'evil.php'
96+
}
97+
98+
def "does not match filename* extended parameter as filename"() {
99+
expect:
100+
MultipartHelper.filenameFromContentDisposition("form-data; filename*=UTF-8''evil.php") == null
101+
}
73102
}

0 commit comments

Comments
 (0)