Adding test cases to confirm path traversal attempts are mitigated and
bad characters in URI return 404 Signed-off-by: David Lawrence <david.lawrence@docker.com> (github: endophage)master
							parent
							
								
									1700f518cb
								
							
						
					
					
						commit
						6f1b92621c
					
				|  | @ -2,20 +2,24 @@ package v2 | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
|  | 	"fmt" | ||||||
|  | 	"math/rand" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"net/http/httptest" | 	"net/http/httptest" | ||||||
| 	"reflect" | 	"reflect" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"testing" | 	"testing" | ||||||
|  | 	"time" | ||||||
| 
 | 
 | ||||||
| 	"github.com/gorilla/mux" | 	"github.com/gorilla/mux" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| type routeTestCase struct { | type routeTestCase struct { | ||||||
| 	RequestURI string | 	RequestURI  string | ||||||
| 	Vars       map[string]string | 	ExpectedURI string | ||||||
| 	RouteName  string | 	Vars        map[string]string | ||||||
| 	StatusCode int | 	RouteName   string | ||||||
|  | 	StatusCode  int | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // TestRouter registers a test handler with all the routes and ensures that
 | // TestRouter registers a test handler with all the routes and ensures that
 | ||||||
|  | @ -25,36 +29,7 @@ type routeTestCase struct { | ||||||
| //
 | //
 | ||||||
| // This may go away as the application structure comes together.
 | // This may go away as the application structure comes together.
 | ||||||
| func TestRouter(t *testing.T) { | func TestRouter(t *testing.T) { | ||||||
| 	baseTestRouter(t, "") | 	testCases := []routeTestCase{ | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestRouterWithPrefix(t *testing.T) { |  | ||||||
| 	baseTestRouter(t, "/prefix/") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func baseTestRouter(t *testing.T, prefix string) { |  | ||||||
| 
 |  | ||||||
| 	router := RouterWithPrefix(prefix) |  | ||||||
| 
 |  | ||||||
| 	testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |  | ||||||
| 		testCase := routeTestCase{ |  | ||||||
| 			RequestURI: r.RequestURI, |  | ||||||
| 			Vars:       mux.Vars(r), |  | ||||||
| 			RouteName:  mux.CurrentRoute(r).GetName(), |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		enc := json.NewEncoder(w) |  | ||||||
| 
 |  | ||||||
| 		if err := enc.Encode(testCase); err != nil { |  | ||||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 	}) |  | ||||||
| 
 |  | ||||||
| 	// Startup test server
 |  | ||||||
| 	server := httptest.NewServer(router) |  | ||||||
| 
 |  | ||||||
| 	for _, testcase := range []routeTestCase{ |  | ||||||
| 		{ | 		{ | ||||||
| 			RouteName:  RouteNameBase, | 			RouteName:  RouteNameBase, | ||||||
| 			RequestURI: "/v2/", | 			RequestURI: "/v2/", | ||||||
|  | @ -150,14 +125,90 @@ func baseTestRouter(t *testing.T, prefix string) { | ||||||
| 				"name": "foo/bar/manifests", | 				"name": "foo/bar/manifests", | ||||||
| 			}, | 			}, | ||||||
| 		}, | 		}, | ||||||
| 		{ | 	} | ||||||
| 			RouteName:  RouteNameBlobUploadChunk, |  | ||||||
| 			RequestURI: "/v2/foo/../../blob/uploads/D95306FA-FAD3-4E36-8D41-CF1C93EF8286", |  | ||||||
| 			StatusCode: http.StatusNotFound, |  | ||||||
| 		}, |  | ||||||
| 	} { |  | ||||||
| 		testcase.RequestURI = strings.TrimSuffix(prefix, "/") + testcase.RequestURI |  | ||||||
| 
 | 
 | ||||||
|  | 	checkTestRouter(t, testCases, "", true) | ||||||
|  | 	checkTestRouter(t, testCases, "/prefix/", true) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestRouterWithPathTraversals(t *testing.T) { | ||||||
|  | 	testCases := []routeTestCase{ | ||||||
|  | 		{ | ||||||
|  | 			RouteName:   RouteNameBlobUploadChunk, | ||||||
|  | 			RequestURI:  "/v2/foo/../../blob/uploads/D95306FA-FAD3-4E36-8D41-CF1C93EF8286", | ||||||
|  | 			ExpectedURI: "/blob/uploads/D95306FA-FAD3-4E36-8D41-CF1C93EF8286", | ||||||
|  | 			StatusCode:  http.StatusNotFound, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			// Testing for path traversal attack handling
 | ||||||
|  | 			RouteName:   RouteNameTags, | ||||||
|  | 			RequestURI:  "/v2/foo/../bar/baz/tags/list", | ||||||
|  | 			ExpectedURI: "/v2/bar/baz/tags/list", | ||||||
|  | 			Vars: map[string]string{ | ||||||
|  | 				"name": "bar/baz", | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	checkTestRouter(t, testCases, "", false) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestRouterWithBadCharacters(t *testing.T) { | ||||||
|  | 	if testing.Short() { | ||||||
|  | 		testCases := []routeTestCase{ | ||||||
|  | 			{ | ||||||
|  | 				RouteName:  RouteNameBlobUploadChunk, | ||||||
|  | 				RequestURI: "/v2/foo/blob/uploads/不95306FA-FAD3-4E36-8D41-CF1C93EF8286", | ||||||
|  | 				StatusCode: http.StatusNotFound, | ||||||
|  | 			}, | ||||||
|  | 			{ | ||||||
|  | 				// Testing for path traversal attack handling
 | ||||||
|  | 				RouteName:  RouteNameTags, | ||||||
|  | 				RequestURI: "/v2/foo/不bar/tags/list", | ||||||
|  | 				StatusCode: http.StatusNotFound, | ||||||
|  | 			}, | ||||||
|  | 		} | ||||||
|  | 		checkTestRouter(t, testCases, "", true) | ||||||
|  | 	} else { | ||||||
|  | 		// in the long version we're going to fuzz the router
 | ||||||
|  | 		// with random UTF8 characters not in the 128 bit ASCII range.
 | ||||||
|  | 		// These are not valid characters for the router and we expect
 | ||||||
|  | 		// 404s on every test.
 | ||||||
|  | 		rand.Seed(time.Now().UTC().UnixNano()) | ||||||
|  | 		testCases := make([]routeTestCase, 1000) | ||||||
|  | 		for idx := range testCases { | ||||||
|  | 			testCases[idx] = routeTestCase{ | ||||||
|  | 				RouteName:  RouteNameTags, | ||||||
|  | 				RequestURI: fmt.Sprintf("/v2/%v/%v/tags/list", randomString(10), randomString(10)), | ||||||
|  | 				StatusCode: http.StatusNotFound, | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		checkTestRouter(t, testCases, "", true) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func checkTestRouter(t *testing.T, testCases []routeTestCase, prefix string, deeplyEqual bool) { | ||||||
|  | 	router := RouterWithPrefix(prefix) | ||||||
|  | 
 | ||||||
|  | 	testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||||
|  | 		testCase := routeTestCase{ | ||||||
|  | 			RequestURI: r.RequestURI, | ||||||
|  | 			Vars:       mux.Vars(r), | ||||||
|  | 			RouteName:  mux.CurrentRoute(r).GetName(), | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		enc := json.NewEncoder(w) | ||||||
|  | 
 | ||||||
|  | 		if err := enc.Encode(testCase); err != nil { | ||||||
|  | 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	// Startup test server
 | ||||||
|  | 	server := httptest.NewServer(router) | ||||||
|  | 
 | ||||||
|  | 	for _, testcase := range testCases { | ||||||
|  | 		testcase.RequestURI = strings.TrimSuffix(prefix, "/") + testcase.RequestURI | ||||||
| 		// Register the endpoint
 | 		// Register the endpoint
 | ||||||
| 		route := router.GetRoute(testcase.RouteName) | 		route := router.GetRoute(testcase.RouteName) | ||||||
| 		if route == nil { | 		if route == nil { | ||||||
|  | @ -178,6 +229,10 @@ func baseTestRouter(t *testing.T, prefix string) { | ||||||
| 			// Override default, zero-value
 | 			// Override default, zero-value
 | ||||||
| 			testcase.StatusCode = http.StatusOK | 			testcase.StatusCode = http.StatusOK | ||||||
| 		} | 		} | ||||||
|  | 		if testcase.ExpectedURI == "" { | ||||||
|  | 			// Override default, zero-value
 | ||||||
|  | 			testcase.ExpectedURI = testcase.RequestURI | ||||||
|  | 		} | ||||||
| 
 | 
 | ||||||
| 		if resp.StatusCode != testcase.StatusCode { | 		if resp.StatusCode != testcase.StatusCode { | ||||||
| 			t.Fatalf("unexpected status for %s: %v %v", u, resp.Status, resp.StatusCode) | 			t.Fatalf("unexpected status for %s: %v %v", u, resp.Status, resp.StatusCode) | ||||||
|  | @ -197,13 +252,56 @@ func baseTestRouter(t *testing.T, prefix string) { | ||||||
| 		// Needs to be set out of band
 | 		// Needs to be set out of band
 | ||||||
| 		actualRouteInfo.StatusCode = resp.StatusCode | 		actualRouteInfo.StatusCode = resp.StatusCode | ||||||
| 
 | 
 | ||||||
|  | 		if actualRouteInfo.RequestURI != testcase.ExpectedURI { | ||||||
|  | 			t.Fatalf("URI %v incorrectly parsed, expected %v", actualRouteInfo.RequestURI, testcase.ExpectedURI) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
| 		if actualRouteInfo.RouteName != testcase.RouteName { | 		if actualRouteInfo.RouteName != testcase.RouteName { | ||||||
| 			t.Fatalf("incorrect route %q matched, expected %q", actualRouteInfo.RouteName, testcase.RouteName) | 			t.Fatalf("incorrect route %q matched, expected %q", actualRouteInfo.RouteName, testcase.RouteName) | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		if !reflect.DeepEqual(actualRouteInfo, testcase) { | 		// when testing deep equality, the actualRouteInfo has an empty ExpectedURI, we don't want
 | ||||||
|  | 		// that to make the comparison fail. We're otherwise done with the testcase so empty the
 | ||||||
|  | 		// testcase.ExpectedURI
 | ||||||
|  | 		testcase.ExpectedURI = "" | ||||||
|  | 		if deeplyEqual && !reflect.DeepEqual(actualRouteInfo, testcase) { | ||||||
| 			t.Fatalf("actual does not equal expected: %#v != %#v", actualRouteInfo, testcase) | 			t.Fatalf("actual does not equal expected: %#v != %#v", actualRouteInfo, testcase) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | // -------------- START LICENSED CODE --------------
 | ||||||
|  | // The following code is derivative of https://github.com/google/gofuzz
 | ||||||
|  | // gofuzz is licensed under the Apache License, Version 2.0, January 2004,
 | ||||||
|  | // a copy of which can be found in the LICENSE file at the root of this
 | ||||||
|  | // repository.
 | ||||||
|  | 
 | ||||||
|  | // These functions allow us to generate strings containing only multibyte
 | ||||||
|  | // characters that are invalid in our URLs. They are used above for fuzzing
 | ||||||
|  | // to ensure we always get 404s on these invalid strings
 | ||||||
|  | type charRange struct { | ||||||
|  | 	first, last rune | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // choose returns a random unicode character from the given range, using the
 | ||||||
|  | // given randomness source.
 | ||||||
|  | func (r *charRange) choose() rune { | ||||||
|  | 	count := int64(r.last - r.first) | ||||||
|  | 	return r.first + rune(rand.Int63n(count)) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | var unicodeRanges = []charRange{ | ||||||
|  | 	{'\u00a0', '\u02af'}, // Multi-byte encoded characters
 | ||||||
|  | 	{'\u4e00', '\u9fff'}, // Common CJK (even longer encodings)
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func randomString(length int) string { | ||||||
|  | 	runes := make([]rune, length) | ||||||
|  | 	for i := range runes { | ||||||
|  | 		runes[i] = unicodeRanges[rand.Intn(len(unicodeRanges))].choose() | ||||||
|  | 	} | ||||||
|  | 	return string(runes) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // -------------- END LICENSED CODE --------------
 | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue