@@ -15,65 +15,113 @@ import (
1515// Regression for GHSA-vfp3-v2gw-7wfq (v4 backport): an encoded path separator (%2F or %5C)
1616// must not let a static file request resolve across a separator and bypass route-level middleware.
1717func TestStaticDirectoryHandler_EncodedSeparatorDoesNotBypassRoute (t * testing.T ) {
18- fsys := fstest.MapFS {
19- "admin/secret.txt" : {Data : []byte ("TOP-SECRET" )},
20- "index.html" : {Data : []byte ("public" )},
21- }
22- e := New ()
23- g := e .Group ("/admin" , func (next HandlerFunc ) HandlerFunc {
24- return func (c Context ) error { return c .String (http .StatusForbidden , "denied" ) }
25- })
26- g .GET ("/*" , func (c Context ) error { return c .String (http .StatusOK , "reached-protected-handler" ) })
27- e .StaticFS ("/" , fsys )
28-
29- cases := []struct {
18+ var testCases = []struct {
19+ name string
3020 target string
3121 wantCode int
3222 wantBody string
3323 }{
34- {"/admin/secret.txt" , http .StatusForbidden , "denied" }, // protected route fires
35- {"/admin%2Fsecret.txt" , http .StatusNotFound , "" }, // encoded slash rejected, no disclosure
36- {"/admin%2fsecret.txt" , http .StatusNotFound , "" }, // lower-case hex variant
37- {"/admin%5Csecret.txt" , http .StatusNotFound , "" }, // encoded backslash (Windows separator) neutralized by path.Clean
38- {"/admin%252Fsecret.txt" , http .StatusNotFound , "" }, // double-encoded: single unescape -> literal filename, not a separator
39- {"/index.html" , http .StatusOK , "public" }, // legitimate static file still served
24+ {
25+ name : "protected route fires" ,
26+ target : "/admin/secret.txt" ,
27+ wantCode : http .StatusForbidden ,
28+ wantBody : "denied" ,
29+ },
30+ {
31+ name : "encoded slash rejected, no disclosure" ,
32+ target : "/admin%2Fsecret.txt" ,
33+ wantCode : http .StatusNotFound ,
34+ wantBody : "" ,
35+ },
36+ {
37+ name : "lower-case hex variant" ,
38+ target : "/admin%2fsecret.txt" ,
39+ wantCode : http .StatusNotFound ,
40+ wantBody : "" ,
41+ },
42+ {
43+ name : "encoded backslash variant - Windows specific related" ,
44+ target : "/admin%5Csecret.txt" ,
45+ wantCode : http .StatusNotFound ,
46+ wantBody : "" ,
47+ },
48+ {
49+ name : "double-encoded: single unescape -> literal filename, not a separator" ,
50+ target : "/admin%252Fsecret.txt" ,
51+ wantCode : http .StatusNotFound ,
52+ wantBody : "" ,
53+ },
54+ {
55+ name : "legitimate static file still served" ,
56+ target : "/index.html" ,
57+ wantCode : http .StatusOK ,
58+ wantBody : "public" ,
59+ },
4060 }
41- for _ , tc := range cases {
42- req := httptest .NewRequest (http .MethodGet , tc .target , nil )
43- rec := httptest .NewRecorder ()
44- e .ServeHTTP (rec , req )
45- assert .Equal (t , tc .wantCode , rec .Code , "GET %s" , tc .target )
46- if tc .wantBody != "" {
47- assert .Equal (t , tc .wantBody , rec .Body .String (), "GET %s" , tc .target )
48- }
49- assert .NotContains (t , rec .Body .String (), "TOP-SECRET" , "GET %s leaked protected file" , tc .target )
61+ for _ , tc := range testCases {
62+ t .Run (tc .name , func (t * testing.T ) {
63+ fsys := fstest.MapFS {
64+ "admin/secret.txt" : {Data : []byte ("TOP-SECRET" )},
65+ "index.html" : {Data : []byte ("public" )},
66+ }
67+ e := New ()
68+ g := e .Group ("/admin" , func (next HandlerFunc ) HandlerFunc {
69+ return func (c Context ) error { return c .String (http .StatusForbidden , "denied" ) }
70+ })
71+ g .GET ("/*" , func (c Context ) error { return c .String (http .StatusOK , "reached-protected-handler" ) })
72+ e .StaticFS ("/" , fsys )
73+
74+ req := httptest .NewRequest (http .MethodGet , tc .target , nil )
75+ rec := httptest .NewRecorder ()
76+ e .ServeHTTP (rec , req )
77+ assert .Equal (t , tc .wantCode , rec .Code , "GET %s" , tc .target )
78+ if tc .wantBody != "" {
79+ assert .Equal (t , tc .wantBody , rec .Body .String (), "GET %s" , tc .target )
80+ }
81+ assert .NotContains (t , rec .Body .String (), "TOP-SECRET" , "GET %s leaked protected file" , tc .target )
82+ })
5083 }
5184}
5285
5386// A Group-mounted StaticFS shares StaticDirectoryHandler, so it must reject the
5487// same encoded separators when served under a non-root prefix.
5588func TestGroupStaticFS_EncodedSeparatorDoesNotBypassRoute (t * testing.T ) {
56- fsys := fstest.MapFS {
57- "admin/secret.txt" : {Data : []byte ("TOP-SECRET" )},
58- "index.html" : {Data : []byte ("public" )},
59- }
60- e := New ()
61- g := e .Group ("/files" )
62- g .StaticFS ("/" , fsys )
63-
64- cases := []struct {
89+ var testCases = []struct {
90+ name string
6591 target string
6692 wantCode int
6793 }{
68- {"/files/index.html" , http .StatusOK },
69- {"/files/admin%2Fsecret.txt" , http .StatusNotFound },
70- {"/files/admin%5Csecret.txt" , http .StatusNotFound },
94+ {
95+ name : "ok" ,
96+ target : "/files/index.html" ,
97+ wantCode : http .StatusOK ,
98+ },
99+ {
100+ name : "nok, encoded slash" ,
101+ target : "/files/admin%2Fsecret.txt" ,
102+ wantCode : http .StatusNotFound ,
103+ },
104+ {
105+ name : "nok encoded backslash" ,
106+ target : "/files/admin%5Csecret.txt" ,
107+ wantCode : http .StatusNotFound ,
108+ },
71109 }
72- for _ , tc := range cases {
73- req := httptest .NewRequest (http .MethodGet , tc .target , nil )
74- rec := httptest .NewRecorder ()
75- e .ServeHTTP (rec , req )
76- assert .Equal (t , tc .wantCode , rec .Code , "GET %s" , tc .target )
77- assert .NotContains (t , rec .Body .String (), "TOP-SECRET" , "GET %s leaked protected file" , tc .target )
110+ for _ , tc := range testCases {
111+ t .Run (tc .name , func (t * testing.T ) {
112+ fsys := fstest.MapFS {
113+ "admin/secret.txt" : {Data : []byte ("TOP-SECRET" )},
114+ "index.html" : {Data : []byte ("public" )},
115+ }
116+ e := New ()
117+ g := e .Group ("/files" )
118+ g .StaticFS ("/" , fsys )
119+
120+ req := httptest .NewRequest (http .MethodGet , tc .target , nil )
121+ rec := httptest .NewRecorder ()
122+ e .ServeHTTP (rec , req )
123+ assert .Equal (t , tc .wantCode , rec .Code , "GET %s" , tc .target )
124+ assert .NotContains (t , rec .Body .String (), "TOP-SECRET" , "GET %s leaked protected file" , tc .target )
125+ })
78126 }
79127}
0 commit comments