mirror of https://github.com/gofiber/fiber.git
Merge 8b85d1947d
into 551570326c
commit
6c6e1690b0
|
@ -99,11 +99,30 @@ func New(root string, cfg ...Config) fiber.Handler {
|
|||
}
|
||||
}
|
||||
|
||||
// Add a leading slash if missing
|
||||
if len(path) > 0 && path[0] != '/' {
|
||||
path = append([]byte("/"), path...)
|
||||
}
|
||||
|
||||
return path
|
||||
// Perform explicit path validation
|
||||
absRoot, err := filepath.Abs(root)
|
||||
if err != nil {
|
||||
fctx.Response.SetStatusCode(fiber.StatusInternalServerError)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Clean the path and resolve it against the root
|
||||
cleanPath := filepath.Clean(utils.UnsafeString(path))
|
||||
absPath := filepath.Join(absRoot, cleanPath)
|
||||
relPath, err := filepath.Rel(absRoot, absPath)
|
||||
|
||||
// Check if the resolved path is within the root
|
||||
if err != nil || strings.HasPrefix(relPath, "..") {
|
||||
fctx.Response.SetStatusCode(fiber.StatusForbidden)
|
||||
return nil
|
||||
}
|
||||
|
||||
return []byte(cleanPath)
|
||||
}
|
||||
|
||||
maxAge := config.MaxAge
|
||||
|
|
|
@ -412,7 +412,7 @@ func Test_Static_Next(t *testing.T) {
|
|||
func Test_Route_Static_Root(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dir := "../../.github/testdata/fs/css"
|
||||
dir := "../../.github/testdata/fs/css" //nolint:goconst // test
|
||||
app := fiber.New()
|
||||
app.Get("/*", New(dir, Config{
|
||||
Browse: true,
|
||||
|
@ -850,3 +850,187 @@ func Test_Static_Compress_WithFileSuffixes(t *testing.T) {
|
|||
require.NoError(t, err, "File should exist")
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Static_PathTraversal(t *testing.T) {
|
||||
// Skip this test if running on Windows
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("Skipping Windows-specific tests")
|
||||
}
|
||||
|
||||
t.Parallel()
|
||||
app := fiber.New()
|
||||
|
||||
// Serve only from "../../.github/testdata/fs/css"
|
||||
// This directory should contain `style.css` but not `index.html` or anything above it.
|
||||
rootDir := "../../.github/testdata/fs/css"
|
||||
app.Get("/*", New(rootDir))
|
||||
|
||||
// A valid request: should succeed
|
||||
validReq := httptest.NewRequest(fiber.MethodGet, "/style.css", nil)
|
||||
validResp, err := app.Test(validReq)
|
||||
require.NoError(t, err, "app.Test(req)")
|
||||
require.Equal(t, 200, validResp.StatusCode, "Status code")
|
||||
require.Equal(t, fiber.MIMETextCSSCharsetUTF8, validResp.Header.Get(fiber.HeaderContentType))
|
||||
validBody, err := io.ReadAll(validResp.Body)
|
||||
require.NoError(t, err, "app.Test(req)")
|
||||
require.Contains(t, string(validBody), "color")
|
||||
|
||||
// Helper function to assert that a given path is blocked.
|
||||
// Blocked can mean different status codes depending on what triggered the block.
|
||||
// We'll accept 400 or 404 as "blocked" statuses:
|
||||
// - 404 is the expected blocked response in most cases.
|
||||
// - 400 might occur if fasthttp rejects the request before it's even processed (e.g., null bytes).
|
||||
assertTraversalBlocked := func(path string) {
|
||||
req := httptest.NewRequest(fiber.MethodGet, path, nil)
|
||||
resp, err := app.Test(req)
|
||||
require.NoError(t, err, "app.Test(req)")
|
||||
|
||||
status := resp.StatusCode
|
||||
require.Truef(t, status == 400 || status == 404,
|
||||
"Status code for path traversal %s should be 400 or 404, got %d", path, status)
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
// If we got a 404, we expect the "Cannot GET" message because that's how fiber handles NotFound by default.
|
||||
if status == 404 {
|
||||
require.Contains(t, string(body), "Cannot GET",
|
||||
"Blocked traversal should have a Cannot GET message for %s", path)
|
||||
} else {
|
||||
require.Contains(t, string(body), "Are you a hacker?",
|
||||
"Blocked traversal should have a Cannot GET message for %s", path)
|
||||
}
|
||||
}
|
||||
|
||||
// Basic attempts to escape the directory
|
||||
assertTraversalBlocked("/index.html..")
|
||||
assertTraversalBlocked("/style.css..")
|
||||
assertTraversalBlocked("/../index.html")
|
||||
assertTraversalBlocked("/../../index.html")
|
||||
assertTraversalBlocked("/../../../index.html")
|
||||
|
||||
// Attempts with double slashes
|
||||
assertTraversalBlocked("//../index.html")
|
||||
assertTraversalBlocked("/..//index.html")
|
||||
|
||||
// Encoded attempts: `%2e` is '.' and `%2f` is '/'
|
||||
assertTraversalBlocked("/..%2findex.html") // ../index.html
|
||||
assertTraversalBlocked("/%2e%2e/index.html") // ../index.html
|
||||
assertTraversalBlocked("/%2e%2e%2f%2e%2e/secret") // ../../../secret
|
||||
|
||||
// Mixed encoded and normal attempts
|
||||
assertTraversalBlocked("/%2e%2e/../index.html") // ../../index.html
|
||||
assertTraversalBlocked("/..%2f..%2fsecret.json") // ../../../secret.json
|
||||
|
||||
// Attempts with current directory references
|
||||
assertTraversalBlocked("/./../index.html")
|
||||
assertTraversalBlocked("/././../index.html")
|
||||
|
||||
// Trailing slashes
|
||||
assertTraversalBlocked("/../")
|
||||
assertTraversalBlocked("/../../")
|
||||
|
||||
// Attempts to load files from an absolute path outside the root
|
||||
assertTraversalBlocked("/" + rootDir + "/../../index.html")
|
||||
|
||||
// Additional edge cases:
|
||||
|
||||
// Double-encoded `..`
|
||||
assertTraversalBlocked("/%252e%252e/index.html") // double-encoded .. -> ../index.html after double decoding
|
||||
|
||||
// Multiple levels of encoding and traversal
|
||||
assertTraversalBlocked("/%2e%2e%2F..%2f%2e%2e%2fWINDOWS") // multiple ups and unusual pattern
|
||||
assertTraversalBlocked("/%2e%2e%2F..%2f%2e%2e%2f%2e%2e/secret") // more complex chain of ../
|
||||
|
||||
// Null byte attempts
|
||||
assertTraversalBlocked("/index.html%00.jpg")
|
||||
assertTraversalBlocked("/%00index.html")
|
||||
assertTraversalBlocked("/somefolder%00/something")
|
||||
assertTraversalBlocked("/%00/index.html")
|
||||
|
||||
// Attempts to access known system files
|
||||
assertTraversalBlocked("/etc/passwd")
|
||||
assertTraversalBlocked("/etc/")
|
||||
|
||||
// Complex mixed attempts with encoded slashes and dots
|
||||
assertTraversalBlocked("/..%2F..%2F..%2F..%2Fetc%2Fpasswd")
|
||||
|
||||
// Attempts inside subdirectories with encoded traversal
|
||||
assertTraversalBlocked("/somefolder/%2e%2e%2findex.html")
|
||||
assertTraversalBlocked("/somefolder/%2e%2e%2f%2e%2e%2findex.html")
|
||||
|
||||
// Backslash encoded attempts
|
||||
assertTraversalBlocked("/%5C..%5Cindex.html")
|
||||
}
|
||||
|
||||
func Test_Static_PathTraversal_WindowsOnly(t *testing.T) {
|
||||
// Skip this test if not running on Windows
|
||||
if runtime.GOOS != "windows" {
|
||||
t.Skip("Skipping Windows-specific tests")
|
||||
}
|
||||
|
||||
t.Parallel()
|
||||
app := fiber.New()
|
||||
|
||||
// Serve only from "../../.github/testdata/fs/css"
|
||||
rootDir := "../../.github/testdata/fs/css"
|
||||
app.Get("/*", New(rootDir))
|
||||
|
||||
// A valid request (relative path without backslash):
|
||||
validReq := httptest.NewRequest(fiber.MethodGet, "/style.css", nil)
|
||||
validResp, err := app.Test(validReq)
|
||||
require.NoError(t, err, "app.Test(req)")
|
||||
require.Equal(t, 200, validResp.StatusCode, "Status code for valid file on Windows")
|
||||
body, err := io.ReadAll(validResp.Body)
|
||||
require.NoError(t, err, "app.Test(req)")
|
||||
require.Contains(t, string(body), "color")
|
||||
|
||||
// Helper to test blocked responses
|
||||
assertTraversalBlocked := func(path string) {
|
||||
req := httptest.NewRequest(fiber.MethodGet, path, nil)
|
||||
resp, err := app.Test(req)
|
||||
require.NoError(t, err, "app.Test(req)")
|
||||
|
||||
// We expect a blocked request to return either 400 or 404
|
||||
status := resp.StatusCode
|
||||
require.Containsf(t, []int{400, 404}, status,
|
||||
"Status code for path traversal %s should be 400 or 404, got %d", path, status)
|
||||
|
||||
// If it's a 404, we expect a "Cannot GET" message
|
||||
if status == 404 {
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, string(body), "Cannot GET",
|
||||
"Blocked traversal should have a 'Cannot GET' message for %s", path)
|
||||
} else {
|
||||
require.Contains(t, string(body), "Are you a hacker?",
|
||||
"Blocked traversal should have a Cannot GET message for %s", path)
|
||||
}
|
||||
}
|
||||
|
||||
// Windows-specific traversal attempts
|
||||
// Backslashes are treated as directory separators on Windows.
|
||||
assertTraversalBlocked("/..\\index.html")
|
||||
assertTraversalBlocked("/..\\..\\index.html")
|
||||
|
||||
// Attempt with a path that might try to reference Windows drives or absolute paths
|
||||
// Note: These are artificial tests to ensure no drive-letter escapes are allowed.
|
||||
assertTraversalBlocked("/C:\\Windows\\System32\\cmd.exe")
|
||||
assertTraversalBlocked("/C:/Windows/System32/cmd.exe")
|
||||
|
||||
// Attempt with UNC-like paths (though unlikely in a web context, good to test)
|
||||
assertTraversalBlocked("//server\\share\\secret.txt")
|
||||
|
||||
// Attempt using a mixture of forward and backward slashes
|
||||
assertTraversalBlocked("/..\\..\\/index.html")
|
||||
|
||||
// Attempt that includes a null-byte on Windows
|
||||
assertTraversalBlocked("/index.html%00.txt")
|
||||
|
||||
// Check behavior on an obviously non-existent and suspicious file
|
||||
assertTraversalBlocked("/\\this\\path\\does\\not\\exist\\..")
|
||||
|
||||
// Attempts involving relative traversal and current directory reference
|
||||
assertTraversalBlocked("/.\\../index.html")
|
||||
assertTraversalBlocked("/./..\\index.html")
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue