Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

637638 limit number of form parameters to avoid DOS

  • Loading branch information...
commit 085c79d7d6cfbccc02821ffdb64968593df3e0bf 1 parent f62c6ed
Greg Wilkins authored
25  jetty-server/src/main/java/org/eclipse/jetty/server/Request.java
@@ -101,6 +101,14 @@
101 101
  * to avoid reparsing headers and cookies that are likely to be the same for 
102 102
  * requests from the same connection.
103 103
  * 
  104
+ * <p>
  105
+ * The form content that a request can process is limited to protect from Denial of Service 
  106
+ * attacks. The size in bytes is limited by {@link ContextHandler#getMaxFormContentSize()} or if there is no 
  107
+ * context then the "org.eclipse.jetty.server.Request.maxFormContentSize" {@link Server} attribute.  
  108
+ * The number of parameters keys is limited by {@link ContextHandler#getMaxFormKeys()} or if there is no
  109
+ * context then the "org.eclipse.jetty.server.Request.maxFormKeys" {@link Server} attribute. 
  110
+ * 
  111
+ * 
104 112
  */
105 113
 public class Request implements HttpServletRequest
106 114
 {
@@ -231,7 +239,7 @@ public void extractParameters()
231 239
             if (content_type != null && content_type.length() > 0)
232 240
             {
233 241
                 content_type = HttpFields.valueParameters(content_type, null);
234  
-
  242
+                
235 243
                 if (MimeTypes.FORM_ENCODED.equalsIgnoreCase(content_type) && _inputState==__NONE &&
236 244
                         (HttpMethods.POST.equals(getMethod()) || HttpMethods.PUT.equals(getMethod())))
237 245
                 {
@@ -241,16 +249,21 @@ public void extractParameters()
241 249
                         try
242 250
                         {
243 251
                             int maxFormContentSize=-1;
  252
+                            int maxFormKeys=-1;
244 253
 
245 254
                             if (_context!=null)
  255
+                            {
246 256
                                 maxFormContentSize=_context.getContextHandler().getMaxFormContentSize();
  257
+                                maxFormKeys=_context.getContextHandler().getMaxFormKeys();
  258
+                            }
247 259
                             else
248 260
                             {
249  
-                                Integer size = (Integer)_connection.getConnector().getServer().getAttribute("org.eclipse.jetty.server.Request.maxFormContentSize");
250  
-                                if (size!=null)
251  
-                                    maxFormContentSize =size.intValue();
  261
+                                Number size = (Number)_connection.getConnector().getServer().getAttribute("org.eclipse.jetty.server.Request.maxFormContentSize");
  262
+                                maxFormContentSize=size==null?200000:size.intValue();
  263
+                                Number keys = (Number)_connection.getConnector().getServer().getAttribute("org.eclipse.jetty.server.Request.maxFormKeys");
  264
+                                maxFormKeys =keys==null?1000:keys.intValue();
252 265
                             }
253  
-
  266
+                            
254 267
                             if (content_length>maxFormContentSize && maxFormContentSize > 0)
255 268
                             {
256 269
                                 throw new IllegalStateException("Form too large"+content_length+">"+maxFormContentSize);
@@ -258,7 +271,7 @@ public void extractParameters()
258 271
                             InputStream in = getInputStream();
259 272
 
260 273
                             // Add form params to query params
261  
-                            UrlEncoded.decodeTo(in, _baseParameters, encoding,content_length<0?maxFormContentSize:-1);
  274
+                            UrlEncoded.decodeTo(in, _baseParameters, encoding,content_length<0?maxFormContentSize:-1,maxFormKeys);
262 275
                         }
263 276
                         catch (IOException e)
264 277
                         {
21  jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java
@@ -120,6 +120,7 @@ public static Context getCurrentContext()
120 120
     private EventListener[] _eventListeners;
121 121
     private Logger _logger;
122 122
     private boolean _allowNullPathInfo;
  123
+    private int _maxFormKeys = Integer.getInteger("org.eclipse.jetty.server.Request.maxFormKeys",1000).intValue();
123 124
     private int _maxFormContentSize = Integer.getInteger("org.eclipse.jetty.server.Request.maxFormContentSize",200000).intValue();
124 125
     private boolean _compactPath = false;
125 126
     private boolean _aliases = false;
@@ -1348,12 +1349,32 @@ public int getMaxFormContentSize()
1348 1349
     }
1349 1350
 
1350 1351
     /* ------------------------------------------------------------ */
  1352
+    /**
  1353
+     * Set the maximum size of a form post, to protect against DOS attacks from large forms.
  1354
+     * @param maxSize
  1355
+     */
1351 1356
     public void setMaxFormContentSize(int maxSize)
1352 1357
     {
1353 1358
         _maxFormContentSize = maxSize;
1354 1359
     }
1355 1360
 
1356 1361
     /* ------------------------------------------------------------ */
  1362
+    public int getMaxFormKeys()
  1363
+    {
  1364
+        return _maxFormKeys;
  1365
+    }
  1366
+
  1367
+    /* ------------------------------------------------------------ */
  1368
+    /**
  1369
+     * Set the maximum number of form Keys to protect against DOS attack from crafted hash keys.
  1370
+     * @param max
  1371
+     */
  1372
+    public void setMaxFormKeys(int max)
  1373
+    {
  1374
+        _maxFormKeys = max;
  1375
+    }
  1376
+
  1377
+    /* ------------------------------------------------------------ */
1357 1378
     /**
1358 1379
      * @return True if URLs are compacted to replace multiple '/'s with a single '/'
1359 1380
      */
62  jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java
@@ -19,12 +19,16 @@
19 19
 import static org.junit.Assert.assertSame;
20 20
 import static org.junit.Assert.assertTrue;
21 21
 
  22
+import java.io.BufferedReader;
  23
+import java.io.File;
  24
+import java.io.FileReader;
22 25
 import java.io.IOException;
23 26
 import java.io.InputStream;
24 27
 import java.io.Reader;
25 28
 import java.util.ArrayList;
26 29
 import java.util.Arrays;
27 30
 import java.util.Enumeration;
  31
+import java.util.HashMap;
28 32
 import java.util.Map;
29 33
 
30 34
 import javax.servlet.ServletException;
@@ -34,9 +38,11 @@
34 38
 
35 39
 import junit.framework.Assert;
36 40
 
  41
+import org.eclipse.jetty.http.MimeTypes;
37 42
 import org.eclipse.jetty.server.handler.AbstractHandler;
38 43
 import org.eclipse.jetty.util.IO;
39 44
 import org.eclipse.jetty.util.StringUtil;
  45
+import org.eclipse.jetty.util.log.Log;
40 46
 import org.junit.After;
41 47
 import org.junit.Before;
42 48
 import org.junit.Test;
@@ -740,6 +746,56 @@ public boolean check(HttpServletRequest request,HttpServletResponse response)
740 746
         assertEquals(null,cookie[1]);
741 747
     }
742 748
 
  749
+
  750
+    @Test
  751
+    public void testHashDOS() throws Exception
  752
+    {
  753
+        _server.setAttribute("org.eclipse.jetty.server.Request.maxFormContentSize",-1);
  754
+        _server.setAttribute("org.eclipse.jetty.server.Request.maxFormKeys",1000);
  755
+        
  756
+        // This file is not distributed - as it is dangerous
  757
+        File evil_keys = new File("/tmp/keys_mapping_to_zero_2m");
  758
+        if (!evil_keys.exists())
  759
+        {
  760
+            Log.info("testHashDOS skipped");
  761
+            return;
  762
+        }
  763
+        
  764
+        BufferedReader in = new BufferedReader(new FileReader(evil_keys));
  765
+        StringBuilder buf = new StringBuilder(4000000);
  766
+        
  767
+        String key=null;
  768
+        buf.append("a=b");
  769
+        while((key=in.readLine())!=null)
  770
+        {
  771
+            buf.append("&").append(key).append("=").append("x");
  772
+        }
  773
+        buf.append("&c=d");
  774
+        
  775
+        _handler._checker = new RequestTester()
  776
+        {
  777
+            public boolean check(HttpServletRequest request,HttpServletResponse response)
  778
+            {
  779
+                return "b".equals(request.getParameter("a")) && request.getParameter("c")==null;
  780
+            }
  781
+        };
  782
+
  783
+        String request="POST / HTTP/1.1\r\n"+
  784
+        "Host: whatever\r\n"+
  785
+        "Content-Type: "+MimeTypes.FORM_ENCODED+"\r\n"+
  786
+        "Content-Length: "+buf.length()+"\r\n"+
  787
+        "Connection: close\r\n"+
  788
+        "\r\n"+
  789
+        buf;
  790
+        
  791
+        long start=System.currentTimeMillis();
  792
+        String response = _connector.getResponses(request);
  793
+        assertTrue(response.contains("200 OK"));
  794
+        long now=System.currentTimeMillis();
  795
+        assertTrue((now-start)<5000);
  796
+    }
  797
+    
  798
+    
743 799
     interface RequestTester
744 800
     {
745 801
         boolean check(HttpServletRequest request,HttpServletResponse response) throws IOException;
@@ -754,13 +810,15 @@ public void handle(String target, Request baseRequest, HttpServletRequest reques
754 810
         {
755 811
             ((Request)request).setHandled(true);
756 812
 
757  
-            if (request.getContentLength()>0)
  813
+            if (request.getContentLength()>0 && !MimeTypes.FORM_ENCODED.equals(request.getContentType()))
758 814
                 _content=IO.toString(request.getInputStream());
759  
-
  815
+            
760 816
             if (_checker!=null && _checker.check(request,response))
761 817
                 response.setStatus(200);
762 818
             else
763 819
                 response.sendError(500);
  820
+            
  821
+
764 822
         }
765 823
     }
766 824
 }
63  jetty-util/src/main/java/org/eclipse/jetty/util/UrlEncoded.java
@@ -16,6 +16,7 @@
16 16
 import java.io.IOException;
17 17
 import java.io.InputStream;
18 18
 import java.io.InputStreamReader;
  19
+import java.io.StringWriter;
19 20
 import java.io.UnsupportedEncodingException;
20 21
 import java.util.Iterator;
21 22
 import java.util.Map;
@@ -78,13 +79,13 @@ public UrlEncoded(String s, String charset)
78 79
     /* ----------------------------------------------------------------- */
79 80
     public void decode(String query)
80 81
     {
81  
-        decodeTo(query,this,ENCODING);
  82
+        decodeTo(query,this,ENCODING,-1);
82 83
     }
83 84
     
84 85
     /* ----------------------------------------------------------------- */
85 86
     public void decode(String query,String charset)
86 87
     {
87  
-        decodeTo(query,this,charset);
  88
+        decodeTo(query,this,charset,-1);
88 89
     }
89 90
     
90 91
     /* -------------------------------------------------------------- */
@@ -178,6 +179,15 @@ else if (equalsForNullValue)
178 179
      */
179 180
     public static void decodeTo(String content, MultiMap map, String charset)
180 181
     {
  182
+        decodeTo(content,map,charset,-1);
  183
+    }
  184
+    
  185
+    /* -------------------------------------------------------------- */
  186
+    /** Decoded parameters to Map.
  187
+     * @param content the string containing the encoded parameters
  188
+     */
  189
+    public static void decodeTo(String content, MultiMap map, String charset, int maxKeys)
  190
+    {
181 191
         if (charset==null)
182 192
             charset=ENCODING;
183 193
 
@@ -208,6 +218,11 @@ else if (value!=null&&value.length()>0)
208 218
                       }
209 219
                       key = null;
210 220
                       value=null;
  221
+                      if (maxKeys>0 && map.size()>maxKeys)
  222
+                      {
  223
+                          LOG.warn("maxFormKeys limit exceeded keys>{}",maxKeys);
  224
+                          return;
  225
+                      }
211 226
                       break;
212 227
                   case '=':
213 228
                       if (key!=null)
@@ -343,9 +358,10 @@ else if (buffer.length()>0)
343 358
     /** Decoded parameters to Map.
344 359
      * @param in InputSteam to read
345 360
      * @param map MultiMap to add parameters to
346  
-     * @param maxLength maximum length of content to read 0r -1 for no limit
  361
+     * @param maxLength maximum length of content to read or -1 for no limit
  362
+     * @param maxLength maximum number of keys to read or -1 for no limit
347 363
      */
348  
-    public static void decode88591To(InputStream in, MultiMap map, int maxLength)
  364
+    public static void decode88591To(InputStream in, MultiMap map, int maxLength, int maxKeys)
349 365
     throws IOException
350 366
     {
351 367
         synchronized(map)
@@ -375,6 +391,11 @@ else if (value!=null&&value.length()>0)
375 391
                         }
376 392
                         key = null;
377 393
                         value=null;
  394
+                        if (maxKeys>0 && map.size()>maxKeys)
  395
+                        {
  396
+                            LOG.warn("maxFormKeys limit exceeded keys>{}",maxKeys);
  397
+                            return;
  398
+                        }
378 399
                         break;
379 400
                         
380 401
                     case '=':
@@ -423,9 +444,10 @@ else if (buffer.length()>0)
423 444
     /** Decoded parameters to Map.
424 445
      * @param in InputSteam to read
425 446
      * @param map MultiMap to add parameters to
426  
-     * @param maxLength maximum length of content to read 0r -1 for no limit
  447
+     * @param maxLength maximum length of content to read or -1 for no limit
  448
+     * @param maxLength maximum number of keys to read or -1 for no limit
427 449
      */
428  
-    public static void decodeUtf8To(InputStream in, MultiMap map, int maxLength)
  450
+    public static void decodeUtf8To(InputStream in, MultiMap map, int maxLength, int maxKeys)
429 451
     throws IOException
430 452
     {
431 453
         synchronized(map)
@@ -455,6 +477,11 @@ else if (value!=null&&value.length()>0)
455 477
                         }
456 478
                         key = null;
457 479
                         value=null;
  480
+                        if (maxKeys>0 && map.size()>maxKeys)
  481
+                        {
  482
+                            LOG.warn("maxFormKeys limit exceeded keys>{}",maxKeys);
  483
+                            return;
  484
+                        }
458 485
                         break;
459 486
                         
460 487
                     case '=':
@@ -500,25 +527,20 @@ else if (buffer.length()>0)
500 527
     }
501 528
     
502 529
     /* -------------------------------------------------------------- */
503  
-    public static void decodeUtf16To(InputStream in, MultiMap map, int maxLength) throws IOException
  530
+    public static void decodeUtf16To(InputStream in, MultiMap map, int maxLength, int maxKeys) throws IOException
504 531
     {
505 532
         InputStreamReader input = new InputStreamReader(in,StringUtil.__UTF16);
506  
-        StringBuffer buf = new StringBuffer();
507  
-
508  
-        int c;
509  
-        int length=0;
510  
-        if (maxLength<0)
511  
-            maxLength=Integer.MAX_VALUE;
512  
-        while ((c=input.read())>0 && length++<maxLength)
513  
-            buf.append((char)c);
514  
-        decodeTo(buf.toString(),map,ENCODING);
  533
+        StringWriter buf = new StringWriter(8192);
  534
+        IO.copy(input,buf,maxLength);
  535
+        
  536
+        decodeTo(buf.getBuffer().toString(),map,ENCODING,maxKeys);
515 537
     }
516 538
     
517 539
     /* -------------------------------------------------------------- */
518 540
     /** Decoded parameters to Map.
519 541
      * @param in the stream containing the encoded parameters
520 542
      */
521  
-    public static void decodeTo(InputStream in, MultiMap map, String charset, int maxLength)
  543
+    public static void decodeTo(InputStream in, MultiMap map, String charset, int maxLength, int maxKeys)
522 544
     throws IOException
523 545
     {
524 546
         //no charset present, use the configured default
@@ -527,22 +549,21 @@ public static void decodeTo(InputStream in, MultiMap map, String charset, int ma
527 549
            charset=ENCODING;
528 550
         }
529 551
             
530  
-            
531 552
         if (StringUtil.__UTF8.equalsIgnoreCase(charset))
532 553
         {
533  
-            decodeUtf8To(in,map,maxLength);
  554
+            decodeUtf8To(in,map,maxLength,maxKeys);
534 555
             return;
535 556
         }
536 557
         
537 558
         if (StringUtil.__ISO_8859_1.equals(charset))
538 559
         {
539  
-            decode88591To(in,map,maxLength);
  560
+            decode88591To(in,map,maxLength,maxKeys);
540 561
             return;
541 562
         }
542 563
 
543 564
         if (StringUtil.__UTF16.equalsIgnoreCase(charset)) // Should be all 2 byte encodings
544 565
         {
545  
-            decodeUtf16To(in,map,maxLength);
  566
+            decodeUtf16To(in,map,maxLength,maxKeys);
546 567
             return;
547 568
         }
548 569
         
4  jetty-util/src/test/java/org/eclipse/jetty/util/URLEncodedTest.java
@@ -178,7 +178,7 @@ public void testUrlEncodedStream()
178 178
         {
179 179
             ByteArrayInputStream in = new ByteArrayInputStream("name\n=value+%30&name1=&name2&n\u00e3me3=value+3".getBytes(charsets[i][0]));
180 180
             MultiMap m = new MultiMap();
181  
-            UrlEncoded.decodeTo(in, m, charsets[i][1], -1);
  181
+            UrlEncoded.decodeTo(in, m, charsets[i][1], -1,-1);
182 182
             System.err.println(m);
183 183
             assertEquals(i+" stream length",4,m.size());
184 184
             assertEquals(i+" stream name\\n","value 0",m.getString("name\n"));
@@ -192,7 +192,7 @@ public void testUrlEncodedStream()
192 192
         {
193 193
             ByteArrayInputStream in2 = new ByteArrayInputStream ("name=%83e%83X%83g".getBytes());
194 194
             MultiMap m2 = new MultiMap();
195  
-            UrlEncoded.decodeTo(in2, m2, "Shift_JIS", -1);
  195
+            UrlEncoded.decodeTo(in2, m2, "Shift_JIS", -1,-1);
196 196
             assertEquals("stream length",1,m2.size());
197 197
             assertEquals("stream name","\u30c6\u30b9\u30c8",m2.getString("name"));
198 198
         }

0 notes on commit 085c79d

Please sign in to comment.
Something went wrong with that request. Please try again.