github.com/google/go-safeweb@v0.0.0-20231219055052-64d8cfc90fbb/internal/requesttesting/formparams/form_params_test.go (about) 1 // Copyright 2020 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // https://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package formparams_test provides tests to inspect the behaviour of form 16 // parameters parsing in HTTP requests. 17 package formparams_test 18 19 import ( 20 "bytes" 21 "context" 22 "net/http" 23 "strconv" 24 "testing" 25 26 "github.com/google/go-cmp/cmp" 27 "github.com/google/go-safeweb/internal/requesttesting" 28 ) 29 30 const status200OK = "HTTP/1.1 200 OK\r\n" 31 const status400BadReq = "HTTP/1.1 400 Bad Request\r\n" 32 33 func TestSimpleFormParameters(t *testing.T) { 34 reqBody := "vegetable=potato&fruit=apple" 35 postReq := []byte("POST / HTTP/1.1\r\n" + 36 "Host: localhost:8080\r\n" + 37 "Content-Type: application/x-www-form-urlencoded; charset=ASCII\r\n" + 38 "Content-Length: " + strconv.Itoa(len(reqBody)) + "\r\n" + 39 "\r\n" + 40 reqBody + "\r\n" + 41 "\r\n") 42 resp, err := requesttesting.MakeRequest(context.Background(), postReq, func(req *http.Request) { 43 if err := req.ParseForm(); err != nil { 44 t.Errorf("req.ParseForm(): got %v, want nil", err) 45 } 46 if !cmp.Equal([]string{"potato"}, req.Form["vegetable"]) { 47 t.Errorf(`req.Form["vegetable"] = %v, want { "potato" }`, req.Form["vegetable"]) 48 } 49 if !cmp.Equal([]string{"apple"}, req.Form["fruit"]) { 50 t.Errorf(`req.Form["fruit"] = %v, want { "apple" }`, req.Form["apple"]) 51 } 52 }) 53 if err != nil { 54 t.Fatalf("MakeRequest(): got err %v, want nil", err) 55 } 56 if !bytes.HasPrefix(resp, []byte(status200OK)) { 57 t.Errorf("response status: got %s, want %s", string(resp), status200OK) 58 } 59 } 60 61 // Test whether passing a POST request with body and without Content-Length 62 // yields a 400 Bad Request 63 func TestFormParametersMissingContentLength(t *testing.T) { 64 postReq := []byte("POST / HTTP/1.1\r\n" + 65 "Host: localhost:8080\r\n" + 66 "Content-Type: application/x-www-form-urlencoded; charset=ASCII\r\n" + 67 "veggie=potato\r\n" + 68 "\r\n") 69 resp, err := requesttesting.MakeRequest(context.Background(), postReq, func(req *http.Request) { 70 t.Fatal("expected handler not to be called when the request is missing the Content-Length header") 71 }) 72 if err != nil { 73 t.Fatalf("MakeRequest(): got err %v, want nil", err) 74 } 75 if !bytes.HasPrefix(resp, []byte(status400BadReq)) { 76 t.Errorf("response status: got %s, want %s", string(resp), status400BadReq) 77 } 78 } 79 80 // Ensure passing a negative Content-Length or one that overflows integers results in a 81 // 400 Bad Request 82 func TestFormParametersBadContentLength(t *testing.T) { 83 tests := []struct { 84 name string 85 req []byte 86 }{ 87 { 88 name: "Negative Content-Length", 89 req: []byte("POST / HTTP/1.1\r\n" + 90 "Host: localhost:8080\r\n" + 91 "Content-Type: application/x-www-form-urlencoded; charset=ASCII\r\n" + 92 "Content-Length: -1\r\n" + 93 "\r\n" + 94 "veggie=potato\r\n" + 95 "\r\n"), 96 }, 97 { 98 name: "Integer Overflow Content-Length", 99 req: []byte("POST / HTTP/1.1\r\n" + 100 "Host: localhost:8080\r\n" + 101 "Content-Type: application/x-www-form-urlencoded; charset=ASCII\r\n" + 102 "Content-Length: 10000000000000000000000000\r\n" + 103 "\r\n" + 104 "veggie=potato\r\n" + 105 "\r\n"), 106 }, 107 } 108 109 for _, test := range tests { 110 t.Run(test.name, func(t *testing.T) { 111 resp, err := requesttesting.MakeRequest(context.Background(), test.req, func(req *http.Request) { 112 t.Error("expected handler not to be called with invalid Content-Length headers") 113 }) 114 if err != nil { 115 t.Fatalf("MakeRequest(): got err %v, want nil", err) 116 } 117 if !bytes.HasPrefix(resp, []byte(status400BadReq)) { 118 t.Errorf("response status: got %s, want %s", string(resp), status400BadReq) 119 } 120 }) 121 } 122 } 123 124 // Tests behaviour when multiple Content-Length values are passed using three 125 // testcases: 126 // a) passing two Content-Length headers containing the same value 127 // b) passing two Content-Length headers containing different values 128 // c) passing one Content-Length header containing a list of equal values 129 // For a) and c), RFC 7320, Section 3.3.2 says that the server can either accept 130 // the request and use the duplicated value or reject it and for b) it should be 131 // rejected. Go will handle a) as a valid request and c) as an invalid requet, as the following two tests demonstrate 132 133 func TestFormParametersValidDuplicateContentLength(t *testing.T) { 134 postReq := []byte("POST / HTTP/1.1\r\n" + 135 "Host: localhost:8080\r\n" + 136 "Content-Type: application/x-www-form-urlencoded; charset=ASCII\r\n" + 137 "Content-Length: 13\r\n" + 138 "Content-Length: 13\r\n" + 139 "\r\n" + 140 "veggie=potato&veggie=a\r\n" + 141 "\r\n") 142 resp, err := requesttesting.MakeRequest(context.Background(), postReq, func(req *http.Request) { 143 want := []string{"13"} 144 got := req.Header["Content-Length"] 145 if diff := cmp.Diff(want, got); diff != "" { 146 t.Errorf("req.Header: got %v, want %v, diff (-want +got): \n%s", got, want, diff) 147 } 148 if err := req.ParseForm(); err != nil { 149 t.Errorf("req.ParseForm(): got %v, want nil", err) 150 } 151 if want := []string{"potato"}; !cmp.Equal(want, req.Form["veggie"]) { 152 t.Errorf(`Form["veggie"]: got %v, want %v`, req.Form["veggie"], want) 153 } 154 155 }) 156 if err != nil { 157 t.Fatalf("MakeRequest(): got err %v, want nil", err) 158 } 159 if !bytes.HasPrefix(resp, []byte(status200OK)) { 160 t.Errorf("response status: got %s, want %s", resp, status200OK) 161 } 162 163 } 164 165 func TestFormParametersInvalidDuplicateContentLength(t *testing.T) { 166 tests := []struct { 167 name string 168 req []byte 169 }{ 170 { 171 name: "Two Content-Length Headers", 172 req: []byte("POST / HTTP/1.1\r\n" + 173 "Host: localhost:8080\r\n" + 174 "Content-Type: application/x-www-form-urlencoded; charset=utf-8\r\n" + 175 "Content-Length: 13\r\n" + 176 "Content-Length: 12\r\n" + 177 "\r\n" + 178 "veggie=potato\r\n" + 179 "\r\n"), 180 }, 181 { 182 name: "List of same value in Content-Length Header", 183 req: []byte("POST / HTTP/1.1\r\n" + 184 "Host: localhost:8080\r\n" + 185 "Content-Type: application/x-www-form-urlencoded; charset=utf-8\r\n" + 186 "Content-Length: 13, 13\r\n" + 187 "\r\n" + 188 "veggie=potato\r\n" + 189 "\r\n"), 190 }, 191 } 192 193 for _, test := range tests { 194 t.Run(test.name, func(t *testing.T) { 195 resp, err := requesttesting.MakeRequest(context.Background(), test.req, func(req *http.Request) { 196 t.Error("expected handler not to be called") 197 }) 198 if err != nil { 199 t.Fatalf("MakeRequest(): got %v, want nil", err) 200 } 201 if !bytes.HasPrefix(resp, []byte(status400BadReq)) { 202 t.Errorf("response status: got %s, want %s", resp, status400BadReq) 203 } 204 }) 205 } 206 } 207 208 // Test whether form parameters with Content-Type: 209 // application/x-www-form-urlencoded that break percent-encoding will be 210 // ignored and following valid query parameters will be discarded 211 func TestFormParametersBreakUrlEncoding(t *testing.T) { 212 postReq := []byte("POST / HTTP/1.1\r\n" + 213 "Host: localhost:8080\r\n" + 214 "Content-Type: application/x-www-form-urlencoded\r\n" + 215 "Content-Length: 22\r\n" + 216 "\r\n" + 217 "veggie=%sc&fruit=apple\r\n" + 218 "\r\n") 219 resp, err := requesttesting.MakeRequest(context.Background(), postReq, func(req *http.Request) { 220 if err := req.ParseForm(); err == nil { 221 t.Errorf(`req.ParseForm(): got %v, want invalid URL escape`, err) 222 } 223 var want []string 224 got := req.Form["veggie"] 225 if diff := cmp.Diff(want, got); diff != "" { 226 t.Errorf(`Form["veggie"]: got %v, want %v`, req.Form["vegetable"], nil) 227 } 228 want = []string{"apple"} 229 got = req.Form["fruit"] 230 if diff := cmp.Diff(want, got); diff != "" { 231 t.Errorf(`Form["fruit"]: got %v, want %v`, req.Form["fruit"], want) 232 } 233 }) 234 if err != nil { 235 t.Fatalf("MakeRequest(): got err %v, want nil", err) 236 } 237 if !bytes.HasPrefix(resp, []byte(status200OK)) { 238 t.Errorf("response status: got %s, want %s", resp, status200OK) 239 } 240 } 241 242 func TestBasicMultipartForm(t *testing.T) { 243 reqBody := "--123\r\n" + 244 "Content-Disposition: form-data; name=\"foo\"\r\n" + 245 "\r\n" + 246 "bar\r\n" + 247 "--123--\r\n" 248 postReq := "POST / HTTP/1.1\r\n" + 249 "Host: localhost:8080\r\n" + 250 "Content-Type: multipart/form-data; boundary=\"123\"\r\n" + 251 "Content-Length: " + strconv.Itoa(len(reqBody)) + "\r\n" + 252 "\r\n" + 253 // CRLF is already in reqBody 254 reqBody + 255 "\r\n" 256 resp, err := requesttesting.MakeRequest(context.Background(), []byte(postReq), func(req *http.Request) { 257 if err := req.ParseMultipartForm(1024); err != nil { 258 t.Fatalf("req.ParseMultipartForm(): want nil, got %v", err) 259 } 260 if want := []string{"bar"}; !cmp.Equal(want, req.Form["foo"]) { 261 t.Errorf("req.Form[foo]: got %v, want %s", req.Form["foo"], want) 262 } 263 }) 264 if err != nil { 265 t.Fatalf("MakeRequest(): got err %v, want nil", err) 266 } 267 if !bytes.HasPrefix(resp, []byte(status200OK)) { 268 t.Errorf("response status: got %s, want %s", resp, status200OK) 269 } 270 } 271 272 // Test whether a multipart/form-data request passed with no Content-Length 273 // will be rejected as a bad request by the server and return a 400 274 func TestMultipartFormNoContentLength(t *testing.T) { 275 // Skipping to ensure GitHub checks are not failing. The testcase shows the body will be ignored and ParseMultiPartForm will fail 276 // with NextPart:EOF. This is rather inconsistent with 277 // application/x-www-form-urlencoded where a missing Content-Length will 278 // return 400 rather than 200. 279 t.Skip() 280 reqBody := "--123\r\n" + 281 "Content-Disposition: form-data; name=\"foo\"\r\n" + 282 "\r\n" + 283 "bar\r\n" + 284 "--123--\r\n" 285 postReq := "POST / HTTP/1.1\r\n" + 286 "Host: localhost:8080\r\n" + 287 "Content-Type: multipart/form-data; boundary=\"123\"\r\n" + 288 "\r\n" + 289 reqBody + 290 "\r\n" 291 resp, err := requesttesting.MakeRequest(context.Background(), []byte(postReq), func(req *http.Request) { 292 t.Error("expected handler not to be called") 293 }) 294 295 if err != nil { 296 t.Fatalf("MakeRequest(): got err %v, want nil", err) 297 } 298 if !bytes.HasPrefix(resp, []byte(status400BadReq)) { 299 t.Errorf("response status: got %s, want %s", resp, status400BadReq) 300 } 301 } 302 303 // Test whether passing a multipart/form-data request results in a 400 Bad 304 // Request server response. 305 func TestMultipartFormSmallContentLength(t *testing.T) { 306 // Skipping to ensure GitHub checks are not failing. The test will reach the 307 // handler and result in the error ParseMultipartForm: NextPart: EOF rather 308 // than be rejected by the server. 309 t.Skip() 310 reqBody := "--123\r\n" + 311 "Content-Disposition: form-data; name=\"foo\"\r\n" + 312 "\r\n" + 313 "bar\r\n" + 314 "--123--\r\n" 315 postReq := "POST / HTTP/1.1\r\n" + 316 "Host: localhost:8080\r\n" + 317 "Content-Type: multipart/form-data; boundary=\"123\"\r\n" + 318 "Content-Length: 10\r\n" + 319 "\r\n" + 320 // reqBody ends with CRLF 321 reqBody + 322 "\r\n" 323 resp, err := requesttesting.MakeRequest(context.Background(), []byte(postReq), func(req *http.Request) { 324 t.Error("expected handler not to be called") 325 }) 326 if err != nil { 327 t.Fatalf("MakeRequest(): got err %v, want nil", err) 328 } 329 if !bytes.HasPrefix(resp, []byte(status400BadReq)) { 330 t.Errorf("response status: got %s, want %s", resp, status400BadReq) 331 } 332 } 333 334 // Test whether passing a multipart/form-data request with bigger 335 // Content-Length results in the server blocking, waiting for the entire request 336 // to be sent. 337 func TestMultipartFormBigContentLength(t *testing.T) { 338 // Skipping to ensure GitHub checks are not failing. The handler will be 339 // actually called as soon as the first part of the body is received, will 340 // return the parsed results and only then block waiting for the rest of the 341 // body. This can lead to a Denial of Service attack. 342 t.Skip() 343 reqBody := "--123\r\n" + 344 "Content-Disposition: form-data; name=\"foo\"\r\n" + 345 "\r\n" + 346 "bar\r\n" + 347 "--123--\r\n" 348 postReq := "POST / HTTP/1.1\r\n" + 349 "Host: localhost:8080\r\n" + 350 "Content-Type: multipart/form-data; boundary=\"123\"\r\n" + 351 "Content-Length: 10000000000000000000\r\n" + 352 "\r\n" + 353 reqBody + 354 "\r\n" 355 _, err := requesttesting.MakeRequest(context.Background(), []byte(postReq), func(req *http.Request) { 356 t.Error("expected handler not to be called") 357 }) 358 if err != nil { 359 t.Fatalf("MakeRequest(): got err %v, want nil", err) 360 } 361 } 362 363 // Test behaviour of multipart/form-data when the boundary appears in the 364 // content as well. According to RFC7578 section 4.1, the request should be 365 // rejected with a 400 Bad Request. 366 func TestMultipartFormIncorrectBoundary(t *testing.T) { 367 // Skipping to ensure GitHub checks are not failing. The request will 368 // actually be considered valid by the server and parsed successfully. 369 t.Skip() 370 reqBody := "--eggplant\r\n" + 371 "Content-Disposition: form-data; name=\"eggplant\"\r\n" + 372 "\r\n" + 373 "eggplant\r\n" + 374 "--eggplant--\r\n" 375 postReq := "POST / HTTP/1.1\r\n" + 376 "Host: localhost:8080\r\n" + 377 "Content-Type: multipart/form-data; boundary=\"eggplant\"\r\n" + 378 "Content-Length: " + strconv.Itoa(len(reqBody)) + "\r\n" + 379 "\r\n" + 380 reqBody + 381 "\r\n" 382 resp, err := requesttesting.MakeRequest(context.Background(), []byte(postReq), func(req *http.Request) { 383 t.Error("expected handler not to be called") 384 }) 385 386 if err != nil { 387 t.Fatalf("MakeRequest(): got err %v, want nil", err) 388 } 389 if !bytes.HasPrefix(resp, []byte(status400BadReq)) { 390 t.Errorf("response status: got %s, want %s", resp, status400BadReq) 391 } 392 } 393 394 // TODO(mihalimara22@): Since req.Form["x"] and req.FormValue("x") return different 395 // types, this aspect should be investigated more in future tests