Description: Fix for Hash collision, see CVE-2011-4461
Origin: https://bugzilla.redhat.com/attachment.cgi?id=556318&action=diff

--- a/modules/jetty/src/main/java/org/mortbay/jetty/handler/ContextHandler.java	2012-01-16 13:35:18.000000000 -0500
+++ b/modules/jetty/src/main/java/org/mortbay/jetty/handler/ContextHandler.java	2012-01-16 14:31:48.000000000 -0500
@@ -118,6 +118,7 @@ public class ContextHandler extends Hand
     private Logger _logger;
     private boolean _shutdown;
     private boolean _allowNullPathInfo;
+    private int _maxFormKeys = Integer.getInteger("org.eclipse.jetty.server.Request.maxFormKeys",1000).intValue();
     private int _maxFormContentSize=Integer.getInteger("org.mortbay.jetty.Request.maxFormContentSize",200000).intValue();
     private boolean _compactPath=false;
 
@@ -1058,11 +1059,30 @@ public class ContextHandler extends Hand
     }
     
     /* ------------------------------------------------------------ */
+    /**
+     * Set the maximum size of a form post, to protect against DOS attacks from large forms.
+     * @param maxSize
+     */
     public void setMaxFormContentSize(int maxSize)
     {
         _maxFormContentSize=maxSize;
     }
 
+    /* ------------------------------------------------------------ */
+    public int getMaxFormKeys()
+    {
+        return _maxFormKeys;
+    }
+
+    /* ------------------------------------------------------------ */
+    /**
+     * Set the maximum number of form Keys to protect against DOS attack from crafted hash keys.
+     * @param max
+     */
+    public void setMaxFormKeys(int max)
+    {
+        _maxFormKeys = max;
+    }
 
     /* ------------------------------------------------------------ */
     /**
--- a/modules/jetty/src/main/java/org/mortbay/jetty/Request.java	2012-01-16 13:24:22.000000000 -0500
+++ b/modules/jetty/src/main/java/org/mortbay/jetty/Request.java	2012-01-16 13:32:38.000000000 -0500
@@ -98,6 +98,13 @@ import org.mortbay.util.ajax.Continuatio
  * to avoid reparsing headers and cookies that are likely to be the same for 
  * requests from the same connection.
  * 
+ * <p>
+ * The form content that a request can process is limited to protect from Denial of Service 
+ * attacks. The size in bytes is limited by {@link ContextHandler#getMaxFormContentSize()} or if there is no 
+ * context then the "org.eclipse.jetty.server.Request.maxFormContentSize" {@link Server} attribute.  
+ * The number of parameters keys is limited by {@link ContextHandler#getMaxFormKeys()} or if there is no
+ * context then the "org.eclipse.jetty.server.Request.maxFormKeys" {@link Server} attribute. 
+ * 
  * @author gregw
  *
  */
@@ -1546,16 +1553,23 @@ public class Request implements HttpServ
                     try
                     {
                         int maxFormContentSize=-1;
-                        
+                        int maxFormKeys=-1;
+
+ 
+
                         if (_context!=null)
+                        {
                             maxFormContentSize=_context.getContextHandler().getMaxFormContentSize();
+                            maxFormKeys=_context.getContextHandler().getMaxFormKeys();
+                        }
                         else
                         {
-                            Integer size = (Integer)_connection.getConnector().getServer().getAttribute("org.mortbay.jetty.Request.maxFormContentSize");
-                            if (size!=null)
-                                maxFormContentSize =size.intValue();
+                            Number size = (Number)_connection.getConnector().getServer().getAttribute("org.eclipse.jetty.server.Request.maxFormContentSize");
+                            maxFormContentSize=size==null?200000:size.intValue();
+                            Number keys = (Number)_connection.getConnector().getServer().getAttribute("org.eclipse.jetty.server.Request.maxFormKeys");
+                            maxFormKeys =keys==null?1000:keys.intValue();
                         }
-                        
+
                         if (content_length>maxFormContentSize && maxFormContentSize > 0)
                         {
                             throw new IllegalStateException("Form too large"+content_length+">"+maxFormContentSize);
@@ -1563,7 +1577,7 @@ public class Request implements HttpServ
                         InputStream in = getInputStream();
                        
                         // Add form params to query params
-                        UrlEncoded.decodeTo(in, _baseParameters, encoding,content_length<0?maxFormContentSize:-1);
+                        UrlEncoded.decodeTo(in, _baseParameters, encoding,content_length<0?maxFormContentSize:-1,maxFormKeys);
                     }
                     catch (IOException e)
                     {
--- a/modules/jetty/src/test/java/org/mortbay/jetty/RequestTest.java	2012-01-16 14:38:35.000000000 -0500
+++ b/modules/jetty/src/test/java/org/mortbay/jetty/RequestTest.java	2012-01-16 14:45:16.000000000 -0500
@@ -15,10 +15,13 @@ 
 
 package org.mortbay.jetty;
 
-
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
 import java.io.IOException;
 import java.io.Reader;
 import java.util.ArrayList;
+import java.util.HashMap;
 
 import javax.servlet.ServletException;
 import javax.servlet.http.Cookie;
@@ -28,10 +31,14 @@ import javax.servlet.http.HttpServletRes
 import junit.framework.TestCase;
 
 import org.mortbay.jetty.Request;
+import org.mortbay.jetty.MimeTypes;
 import org.mortbay.jetty.handler.AbstractHandler;
 import org.mortbay.jetty.handler.HandlerCollection;
 import org.mortbay.util.IO;
 import org.mortbay.util.StringUtil;
+import org.mortbay.log.Log;
+
+
 
 /**
  * @author gregw
@@ -482,7 +489,52 @@ public class RequestTest extends TestCas
         assertEquals("value7" ,cookie[7]);
     }
     
-    
+    public void testHashDOS() throws Exception
+    {
+        _server.setAttribute("org.eclipse.jetty.server.Request.maxFormContentSize",-1);
+        _server.setAttribute("org.eclipse.jetty.server.Request.maxFormKeys",1000);
+        
+        // This file is not distributed - as it is dangerous
+        File evil_keys = new File("/tmp/keys_mapping_to_zero_2m");
+        if (!evil_keys.exists())
+        {
+            Log.info("testHashDOS skipped");
+            return;
+        }
+        
+        BufferedReader in = new BufferedReader(new FileReader(evil_keys));
+        StringBuilder buf = new StringBuilder(4000000);
+        
+        String key=null;
+        buf.append("a=b");
+        while((key=in.readLine())!=null)
+        {
+            buf.append("&").append(key).append("=").append("x");
+        }
+        buf.append("&c=d");
+        
+        _handler._checker = new RequestTester()
+        {
+            public boolean check(HttpServletRequest request,HttpServletResponse response)
+            {
+                return "b".equals(request.getParameter("a")) && request.getParameter("c")==null;
+            }
+        };
+
+        String request="POST / HTTP/1.1\r\n"+
+        "Host: whatever\r\n"+
+        "Content-Type: "+MimeTypes.FORM_ENCODED+"\r\n"+
+        "Content-Length: "+buf.length()+"\r\n"+
+        "Connection: close\r\n"+
+        "\r\n"+
+        buf;
+        
+        long start=System.currentTimeMillis();
+        String response = _connector.getResponses(request);
+        assertTrue(response.contains("200 OK"));
+        long now=System.currentTimeMillis();
+        assertTrue((now-start)<5000);
+    }    
     
     
     interface RequestTester
@@ -498,8 +550,8 @@ public class RequestTest extends TestCas
         public void handle(String target, HttpServletRequest request, HttpServletResponse response, int dispatch) throws IOException, ServletException
         {
             ((Request)request).setHandled(true);
-            
-            if (request.getContentLength()>0)
+
+            if (request.getContentLength()>0 && !MimeTypes.FORM_ENCODED.equals(request.getContentType()))
                 _content=IO.toString(request.getInputStream());
             
             if (_checker!=null && _checker.check(request,response))
--- a/modules/util/src/main/java/org/mortbay/util/UrlEncoded.java	2012-01-16 14:47:05.000000000 -0500
+++ b/modules/util/src/main/java/org/mortbay/util/UrlEncoded.java	2012-01-16 16:59:21.000000000 -0500
@@ -17,9 +17,11 @@ package org.mortbay.util;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
+import java.io.StringWriter;
 import java.io.UnsupportedEncodingException;
 import java.util.Iterator;
 import java.util.Map;
+import org.mortbay.log.Log;
 
 
 /* ------------------------------------------------------------ */
@@ -73,13 +75,13 @@ public class UrlEncoded extends MultiMap
     /* ----------------------------------------------------------------- */
     public void decode(String query)
     {
-        decodeTo(query,this,ENCODING);
+        decodeTo(query,this,ENCODING,-1);
     }
     
     /* ----------------------------------------------------------------- */
     public void decode(String query,String charset)
     {
-        decodeTo(query,this,charset);
+        decodeTo(query,this,charset,-1);
     }
     
     /* -------------------------------------------------------------- */
@@ -174,6 +176,15 @@ public class UrlEncoded extends MultiMap
      */
     public static void decodeTo(String content, MultiMap map, String charset)
     {
+        decodeTo(content,map,charset,-1);
+    }
+    
+    /* -------------------------------------------------------------- */
+    /** Decoded parameters to Map.
+     * @param content the string containing the encoded parameters
+     */
+    public static void decodeTo(String content, MultiMap map, String charset, int maxKeys)
+    {
         if (charset==null)
             charset=ENCODING;
 
@@ -204,6 +215,11 @@ public class UrlEncoded extends MultiMap
                       }
                       key = null;
                       value=null;
+                      if (maxKeys>0 && map.size()>maxKeys)
+                      {
+                          Log.warn("maxFormKeys limit exceeded keys>{}",Integer.valueOf(maxKeys));
+                          return;
+                      }
                       break;
                   case '=':
                       if (key!=null)
@@ -320,9 +336,10 @@ public class UrlEncoded extends MultiMap
     /** Decoded parameters to Map.
      * @param in InputSteam to read
      * @param map MultiMap to add parameters to
-     * @param maxLength maximum length of content to read 0r -1 for no limit
+     * @param maxLength maximum length of content to read or -1 for no limit
+     * @param maxLength maximum number of keys to read or -1 for no limit
      */
-    public static void decode88591To(InputStream in, MultiMap map, int maxLength)
+    public static void decode88591To(InputStream in, MultiMap map, int maxLength, int maxKeys)
     throws IOException
     {
         synchronized(map)
@@ -352,6 +369,11 @@ public class UrlEncoded extends MultiMap
                         }
                         key = null;
                         value=null;
+                        if (maxKeys>0 && map.size()>maxKeys)
+                        {
+                            Log.warn("maxFormKeys limit exceeded keys>{}",Integer.valueOf(maxKeys));
+                            return;
+                        }
                         break;
                         
                     case '=':
@@ -400,9 +422,10 @@ public class UrlEncoded extends MultiMap
     /** Decoded parameters to Map.
      * @param in InputSteam to read
      * @param map MultiMap to add parameters to
-     * @param maxLength maximum length of content to read 0r -1 for no limit
+     * @param maxLength maximum length of content to read or -1 for no limit
+     * @param maxLength maximum number of keys to read or -1 for no limit
      */
-    public static void decodeUtf8To(InputStream in, MultiMap map, int maxLength)
+    public static void decodeUtf8To(InputStream in, MultiMap map, int maxLength, int maxKeys)
     throws IOException
     {
         synchronized(map)
@@ -432,6 +455,11 @@ public class UrlEncoded extends MultiMap
                         }
                         key = null;
                         value=null;
+                        if (maxKeys>0 && map.size()>maxKeys)
+                        {
+                            Log.warn("maxFormKeys limit exceeded keys>{}",Integer.valueOf(maxKeys));
+                            return;
+                        }
                         break;
                         
                     case '=':
@@ -477,43 +505,38 @@ public class UrlEncoded extends MultiMap
     }
     
     /* -------------------------------------------------------------- */
-    public static void decodeUtf16To(InputStream in, MultiMap map, int maxLength) throws IOException
+    public static void decodeUtf16To(InputStream in, MultiMap map, int maxLength, int maxKeys) throws IOException
     {
         InputStreamReader input = new InputStreamReader(in,StringUtil.__UTF16);
-        StringBuffer buf = new StringBuffer();
-
-        int c;
-        int length=0;
-        if (maxLength<0)
-            maxLength=Integer.MAX_VALUE;
-        while ((c=input.read())>0 && length++<maxLength)
-            buf.append((char)c);
-        decodeTo(buf.toString(),map,ENCODING);
+        StringWriter buf = new StringWriter(8192);
+        IO.copy(input,buf,maxLength);
+        
+        decodeTo(buf.getBuffer().toString(),map,ENCODING,maxKeys);
     }
     
     /* -------------------------------------------------------------- */
     /** Decoded parameters to Map.
      * @param in the stream containing the encoded parameters
      */
-    public static void decodeTo(InputStream in, MultiMap map, String charset, int maxLength)
+    public static void decodeTo(InputStream in, MultiMap map, String charset, int maxLength, int maxKeys)
     throws IOException
     {
 
         if (charset==null || StringUtil.__UTF8.equalsIgnoreCase(charset))
         {
-            decodeUtf8To(in,map,maxLength);
+            decodeUtf8To(in,map,maxLength,maxKeys);
             return;
         }
         
         if (StringUtil.__ISO_8859_1.equals(charset))
         {
-            decode88591To(in,map,maxLength);
+            decode88591To(in,map,maxLength,maxKeys);
             return;
         }
 
         if (StringUtil.__UTF16.equalsIgnoreCase(charset)) // Should be all 2 byte encodings
         {
-            decodeUtf16To(in,map,maxLength);
+            decodeUtf16To(in,map,maxLength,maxKeys);
             return;
         }
         
--- a/modules/util/src/test/java/org/mortbay/util/URLEncodedTest.java	2012-01-16 15:19:40.000000000 -0500
+++ b/modules/util/src/test/java/org/mortbay/util/URLEncodedTest.java	2012-01-16 15:20:36.000000000 -0500
@@ -163,7 +163,7 @@ public class URLEncodedTest extends juni
         {
             ByteArrayInputStream in = new ByteArrayInputStream("name\n=value+%30&name1=&name2&n\u00e3me3=value+3".getBytes(charsets[i][0]));
             MultiMap m = new MultiMap();
-            UrlEncoded.decodeTo(in, m, charsets[i][1], -1);
+            UrlEncoded.decodeTo(in, m, charsets[i][1], -1, -1);
             System.err.println(m);
             assertEquals(i+" stream length",4,m.size());
             assertEquals(i+" stream name\\n","value 0",m.getString("name\n"));
@@ -177,7 +177,7 @@ public class URLEncodedTest extends juni
         {
             ByteArrayInputStream in2 = new ByteArrayInputStream ("name=%83e%83X%83g".getBytes());
             MultiMap m2 = new MultiMap();
-            UrlEncoded.decodeTo(in2, m2, "Shift_JIS", -1);
+            UrlEncoded.decodeTo(in2, m2, "Shift_JIS", -1, -1);
             assertEquals("stream length",1,m2.size());
             assertEquals("stream name","\u30c6\u30b9\u30c8",m2.getString("name"));
         }
