Honor parameters order when parsing query and form parameters (#7599) (#7605)

* Honor parameters order when parsing query and form parameters

When parsing the query or form parameters in Request, the values are stored in a MultiMap. This class extends HashMap which does not preserve the order of insertion so a request with parameters "first=1&second=2" might end up in a map where "second" will come first when iterating on the entry set. 

The order is necessary in some case where the request is signed off the body and/or the query parameters. When the order is not preserved, it is impossible to reconstruct the original request sent, unless using the Request::getInputStream which consumes the stream and makes subsequent calls to Request::getParameters to don't return the form parameters which can be misleading. The same behavior applied to query parameters, by using Request::getQueryString, you get the correct order but Request::getParameters will not.

Moreoever, if the application is behind a reverse proxy using Jetty that is proxying using Request::getParameters which consume the request InputStream, it will be completely impossible to reconstruct the original request.

* Added a test with parameter merging

Co-authored-by: Jacques-Etienne Beaudet <jebeaudet@gmail.com>
This commit is contained in:
Joakim Erdfelt 2022-02-16 11:34:30 -06:00 committed by GitHub
parent 292d6cda9f
commit f2e9680431
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 64 additions and 5 deletions

View File

@ -34,6 +34,7 @@ import java.util.EnumSet;
import java.util.Enumeration; import java.util.Enumeration;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
@ -253,6 +254,64 @@ public class RequestTest
assertTrue(responses.startsWith("HTTP/1.1 200")); assertTrue(responses.startsWith("HTTP/1.1 200"));
} }
@Test
public void testParameterExtractionKeepOrderingIntact() throws Exception
{
AtomicReference<Map<String, String[]>> reference = new AtomicReference<>();
_handler._checker = new RequestTester()
{
@Override
public boolean check(HttpServletRequest request, HttpServletResponse response)
{
reference.set(request.getParameterMap());
return true;
}
};
String request = "POST /?first=1&second=2&third=3&fourth=4 HTTP/1.1\r\n" +
"Host: whatever\r\n" +
"Content-Type: application/x-www-form-urlencoded\n" +
"Connection: close\n" +
"Content-Length: 34\n" +
"\n" +
"fifth=5&sixth=6&seventh=7&eighth=8";
String responses = _connector.getResponse(request);
assertTrue(responses.startsWith("HTTP/1.1 200"));
assertThat(new ArrayList<>(reference.get().keySet()), is(Arrays.asList("first", "second", "third", "fourth", "fifth", "sixth", "seventh", "eighth")));
}
@Test
public void testParameterExtractionOrderingWithMerge() throws Exception
{
AtomicReference<Map<String, String[]>> reference = new AtomicReference<>();
_handler._checker = new RequestTester()
{
@Override
public boolean check(HttpServletRequest request, HttpServletResponse response)
{
reference.set(request.getParameterMap());
return true;
}
};
String request = "POST /?a=1&b=2&c=3&a=4 HTTP/1.1\r\n" +
"Host: whatever\r\n" +
"Content-Type: application/x-www-form-urlencoded\n" +
"Connection: close\n" +
"Content-Length: 11\n" +
"\n" +
"c=5&b=6&a=7";
String responses = _connector.getResponse(request);
Map<String, String[]> returnedMap = reference.get();
assertTrue(responses.startsWith("HTTP/1.1 200"));
assertThat(new ArrayList<>(returnedMap.keySet()), is(Arrays.asList("a", "b", "c")));
assertTrue(Arrays.equals(returnedMap.get("a"), new String[]{"1", "4", "7"}));
assertTrue(Arrays.equals(returnedMap.get("b"), new String[]{"2", "6"}));
assertTrue(Arrays.equals(returnedMap.get("c"), new String[]{"3", "5"}));
}
@Test @Test
public void testParamExtractionBadSequence() throws Exception public void testParamExtractionBadSequence() throws Exception
{ {

View File

@ -15,8 +15,8 @@ package org.eclipse.jetty.util;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -26,7 +26,7 @@ import java.util.Map;
* @param <V> the entry type for multimap values * @param <V> the entry type for multimap values
*/ */
@SuppressWarnings("serial") @SuppressWarnings("serial")
public class MultiMap<V> extends HashMap<String, List<V>> public class MultiMap<V> extends LinkedHashMap<String, List<V>>
{ {
public MultiMap() public MultiMap()
{ {
@ -316,13 +316,13 @@ public class MultiMap<V> extends HashMap<String, List<V>>
@Override @Override
public String toString() public String toString()
{ {
Iterator<Entry<String, List<V>>> iter = entrySet().iterator(); Iterator<Map.Entry<String, List<V>>> iter = entrySet().iterator();
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append('{'); sb.append('{');
boolean delim = false; boolean delim = false;
while (iter.hasNext()) while (iter.hasNext())
{ {
Entry<String, List<V>> e = iter.next(); Map.Entry<String, List<V>> e = iter.next();
if (delim) if (delim)
{ {
sb.append(", "); sb.append(", ");
@ -350,7 +350,7 @@ public class MultiMap<V> extends HashMap<String, List<V>>
*/ */
public Map<String, String[]> toStringArrayMap() public Map<String, String[]> toStringArrayMap()
{ {
HashMap<String, String[]> map = new HashMap<String, String[]>(size() * 3 / 2) Map<String, String[]> map = new LinkedHashMap<String, String[]>(size() * 3 / 2)
{ {
@Override @Override
public String toString() public String toString()