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