package middleware import ( "fmt" "path/filepath" "strings" "github.com/gofiber/fiber/v2" ) // PathJail middleware ensures all file path parameters are confined within // the storage root. Any path that resolves outside the jail is rejected // immediately, preventing directory traversal attacks. func PathJail(storageRoot string) fiber.Handler { // Resolve the absolute jail root once at startup absRoot, err := filepath.Abs(storageRoot) if err != nil { panic(fmt.Sprintf("pathjail: invalid storage root: %v", err)) } // Ensure it ends with separator for prefix matching absRootSlash := absRoot + string(filepath.Separator) return func(c *fiber.Ctx) error { // Extract the path parameter from the request. // Handlers attach the relative file path as "filepath" param or query. rawPath := c.Params("*") if rawPath == "" { rawPath = c.Query("path", "") } // If no path param, assume root path '.' if rawPath == "" { rawPath = "." } // Clean and resolve the path cleaned := filepath.Clean(rawPath) // Reject obvious traversal attempts before even joining if strings.Contains(cleaned, "..") { return c.Status(fiber.StatusForbidden).JSON(fiber.Map{ "error": "path traversal detected", }) } // Build the full absolute path within the jail fullPath := filepath.Join(absRoot, cleaned) absPath, err := filepath.Abs(fullPath) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "error": "invalid path", }) } // Final check: resolved path must be within the jail root if absPath != absRoot && !strings.HasPrefix(absPath, absRootSlash) { return c.Status(fiber.StatusForbidden).JSON(fiber.Map{ "error": "access denied: path outside storage boundary", }) } // Store the validated absolute path for downstream handlers c.Locals("resolvedPath", absPath) c.Locals("relativePath", cleaned) return c.Next() } }