@@ -278,10 +278,12 @@ func TestEcho_StaticFS(t *testing.T) {
278278 givenPrefix : "/" ,
279279 givenFs : os .DirFS ("_fixture/" ),
280280 givenEnablePathUnescapingStaticFiles : true ,
281- whenURL : "/open.redirect.hackercom%2f.." ,
282- expectStatus : http .StatusMovedPermanently ,
283- expectHeaderLocation : "/open.redirect.hackercom%2f../" , // location starting with `//open` would be very bad
284- expectBodyStartsWith : "" ,
281+ // `//open.redirect.hackercom/..` resolves to directory but does not end with `/` so redirect is done but this
282+ // redirect can not be to path starting with `//` or `\\` (open redirect)
283+ whenURL : "/%2fopen.redirect.hackercom%2f.." ,
284+ expectStatus : http .StatusMovedPermanently ,
285+ expectHeaderLocation : "/open.redirect.hackercom/../" , // location starting with `//open` would be very bad
286+ expectBodyStartsWith : "" ,
285287 },
286288 {
287289 name : "possible open redirect vulnerability when not unescaping path variables in static handler" ,
@@ -335,59 +337,98 @@ func TestStaticDirectoryHandlerAndRouterInconsistentEscaping(t *testing.T) {
335337 name string
336338 givenEnablePathUnescapingStaticFiles bool
337339 givenRouterUnescapePathParamValues bool
340+ givenRouterUseEscapedPathForMatching bool
338341 whenURL string
339- expectBodyStartsWith string
342+ expectBody string
340343 expectStatus int
341344 }{
342345 {
343346 name : "ok, file is served from not-forbidden path" ,
344347 givenEnablePathUnescapingStaticFiles : false ,
345348 whenURL : "/test.txt" ,
346- expectBodyStartsWith : "test.txt contents\n " ,
349+ expectBody : "test.txt contents\n " ,
347350 expectStatus : http .StatusOK ,
348351 },
349352 {
350353 name : "ok, forbidden path is matched by route wildcard and forbidden by that" ,
351354 givenEnablePathUnescapingStaticFiles : false ,
352355 whenURL : "/admin/private.txt" ,
353- expectBodyStartsWith : "{\" message\" :\" Forbidden\" }\n " ,
356+ expectBody : "{\" message\" :\" Forbidden\" }\n " ,
354357 expectStatus : http .StatusForbidden ,
355358 },
356359 {
357- name : "ok, escaped filename from forbidden path is not escaped and results 404" ,
360+ name : "ok, escaped filename from forbidden path is routed to guarded route" ,
361+ givenEnablePathUnescapingStaticFiles : false ,
362+ givenRouterUnescapePathParamValues : false ,
363+ givenRouterUseEscapedPathForMatching : true , // Router uses escaped path (req.URL.RawPath) for matching
364+ whenURL : "/admin%2fprivate.txt" ,
365+ expectBody : "{\" message\" :\" Forbidden\" }\n " ,
366+ expectStatus : http .StatusForbidden ,
367+ },
368+ {
369+ name : "ok, escaped filename from forbidden path is not unescaped and results 404" ,
358370 givenEnablePathUnescapingStaticFiles : false , // router path escaping and StaticDirectoryHandler is consistent
359371 whenURL : "/admin%2fprivate.txt" ,
360- expectBodyStartsWith : "{\" message\" :\" Not Found\" }\n " ,
372+ expectBody : "{\" message\" :\" Not Found\" }\n " ,
361373 expectStatus : http .StatusNotFound ,
362374 },
363375 {
364- name : "nok, escaped filename from forbidden path is escaped and returns file contents (handler escapes )" ,
376+ name : "nok, escaped filename from forbidden path is unescaped and returns file contents (handler unescapes )" ,
365377 givenEnablePathUnescapingStaticFiles : true , // router path escaping and StaticDirectoryHandler is NOT consistent
378+ givenRouterUnescapePathParamValues : false ,
379+ whenURL : "/admin%2fprivate.txt" ,
380+ expectBody : "public/admin/private.txt - private file\n " ,
381+ expectStatus : http .StatusOK ,
382+ },
383+ {
384+ name : "nok, escaped filename from forbidden path is unescaped and returns file contents (router unescapes)" ,
385+ givenEnablePathUnescapingStaticFiles : false ,
386+ givenRouterUnescapePathParamValues : true , // router path escaping and StaticDirectoryHandler is NOT consistent
366387 whenURL : "/admin%2fprivate.txt" ,
367- expectBodyStartsWith : " private file\n " ,
388+ expectBody : "public/admin/private.txt - private file\n " ,
368389 expectStatus : http .StatusOK ,
369390 },
370391 {
371- name : "nok, escaped filename from forbidden path is escaped and returns file contents (router escapes)" ,
372- givenRouterUnescapePathParamValues : true , // router path escaping and StaticDirectoryHandler is NOT consistent
373- whenURL : "/admin%2fprivate.txt" ,
374- expectBodyStartsWith : "private file\n " ,
375- expectStatus : http .StatusOK ,
392+ name : "nok, unescaped filename from forbidden path is escaped and returns file contents (router unescapes and method unescapes)" ,
393+ givenEnablePathUnescapingStaticFiles : true ,
394+ givenRouterUnescapePathParamValues : true , // consistent path unescaping - makes no difference
395+ whenURL : "/admin%2fprivate.txt" ,
396+ expectBody : "public/admin/private.txt - private file\n " ,
397+ expectStatus : http .StatusOK ,
398+ },
399+ {
400+ name : "nok, escaped filename, resolves to from forbidden path is not routed to guarded route and includes guarded file" ,
401+ givenEnablePathUnescapingStaticFiles : false ,
402+ givenRouterUnescapePathParamValues : false ,
403+ // Router uses escaped path (req.URL.RawPath) for matching, but that file resolves to `admin/private.txt` after path.Clean()
404+ givenRouterUseEscapedPathForMatching : true ,
405+ whenURL : "/assets/../admin%2fprivate.txt" ,
406+ expectBody : "public/admin/private.txt - private file\n " ,
407+ expectStatus : http .StatusOK ,
376408 },
377409 }
378410 for _ , tc := range testCases {
379411 t .Run (tc .name , func (t * testing.T ) {
380412 r := NewRouter (RouterConfig {
381- UnescapePathParamValues : tc .givenRouterUnescapePathParamValues ,
413+ UnescapePathParamValues : tc .givenRouterUnescapePathParamValues ,
414+ UseEscapedPathForMatching : tc .givenRouterUseEscapedPathForMatching ,
382415 })
383416 e := NewWithConfig (Config {
384417 EnablePathUnescapingStaticFiles : tc .givenEnablePathUnescapingStaticFiles ,
385418 Router : r ,
386419 Filesystem : os .DirFS ("./_fixture/dist" ),
387420 })
388421
389- // 1. share folder contents from the server root. This folder actually contains folder `admin` which contents we
390- // want to forbid from downloading
422+ // 0.
423+ // given folder structure:
424+ // private.txt
425+ // public/
426+ // public/index.html
427+ // public/text.txt
428+ // public/admin/private.txt
429+
430+ // 1. share `public/` folder contents from the server root. This folder actually contains subfolder `admin` which
431+ // contents we want to forbid from downloading
391432 e .Static ("/" , "public" )
392433
393434 // 2. naively assume that everything under /admin folder is now forbidden
@@ -402,11 +443,7 @@ func TestStaticDirectoryHandlerAndRouterInconsistentEscaping(t *testing.T) {
402443
403444 assert .Equal (t , tc .expectStatus , rec .Code )
404445 body := rec .Body .String ()
405- if tc .expectBodyStartsWith != "" {
406- assert .True (t , strings .HasPrefix (body , tc .expectBodyStartsWith ))
407- } else {
408- assert .Equal (t , "" , body )
409- }
446+ assert .Equal (t , tc .expectBody , body )
410447 })
411448 }
412449}
0 commit comments