mirror of
https://github.com/filebrowser/filebrowser.git
synced 2026-07-25 17:04:17 +00:00
The in-memory upload cache deleted expired incomplete uploads with a raw os.Remove on the absolute path it had stored, bypassing ScopedFs entirely. Because the stored path is only lexically cleaned (no symlink evaluation) and os.Remove resolves symlinked parent directories, a Create-only user could register an upload and then, within the 3-minute TTL, swap an in-scope ancestor directory for a symlink so the eviction deleted an arbitrary file outside their scope. Carry a removal callback with each cache entry and invoke it on eviction instead of os.Remove. For TUS uploads the callback deletes via the uploading user's scoped filesystem, whose Remove is guarded by the same within() check that CVE-2026-55667 added, so eviction can no longer follow a symlink out of scope. The redis backend ignores the callback (it never deleted partial files). Refs GHSA-m9f5-2232-frp6
86 lines
2.2 KiB
Go
86 lines
2.2 KiB
Go
package fbhttp
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"strconv"
|
|
|
|
"github.com/redis/go-redis/v9"
|
|
)
|
|
|
|
// redisUploadCache is an upload cache for multi replica deployments
|
|
type redisUploadCache struct {
|
|
client *redis.Client
|
|
}
|
|
|
|
func newRedisUploadCache(redisURL string) (*redisUploadCache, error) {
|
|
if redisURL == "" {
|
|
return nil, fmt.Errorf("redis URL is required")
|
|
}
|
|
|
|
opts, err := redis.ParseURL(redisURL)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid redis URL: %w", err)
|
|
}
|
|
|
|
client := redis.NewClient(opts)
|
|
|
|
// Test connection
|
|
if err := client.Ping(context.Background()).Err(); err != nil {
|
|
return nil, fmt.Errorf("failed to connect to redis: %w", err)
|
|
}
|
|
|
|
return &redisUploadCache{client: client}, nil
|
|
}
|
|
|
|
func (c *redisUploadCache) filePathKey(filePath string) string {
|
|
return "filebrowser:upload:" + filePath
|
|
}
|
|
|
|
// Register stores the upload length. The scoped removal callback is unused by
|
|
// the redis backend, which does not delete partial files on eviction.
|
|
func (c *redisUploadCache) Register(filePath string, fileSize int64, _ func() error) {
|
|
err := c.client.Set(context.Background(), c.filePathKey(filePath), fileSize, uploadCacheTTL).Err()
|
|
if err != nil {
|
|
log.Printf("failed to register upload in redis cache: %v", err)
|
|
}
|
|
}
|
|
|
|
func (c *redisUploadCache) Complete(filePath string) {
|
|
err := c.client.Del(context.Background(), c.filePathKey(filePath)).Err()
|
|
if err != nil {
|
|
log.Printf("failed to complete upload in redis cache: %v", err)
|
|
}
|
|
}
|
|
|
|
func (c *redisUploadCache) GetLength(filePath string) (int64, error) {
|
|
result, err := c.client.Get(context.Background(), c.filePathKey(filePath)).Result()
|
|
if err != nil {
|
|
if errors.Is(err, redis.Nil) {
|
|
return 0, fmt.Errorf("no active upload found for the given path")
|
|
}
|
|
return 0, fmt.Errorf("redis error: %w", err)
|
|
}
|
|
|
|
size, err := strconv.ParseInt(result, 10, 64)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("invalid upload length in cache: %w", err)
|
|
}
|
|
|
|
c.Touch(filePath)
|
|
|
|
return size, nil
|
|
}
|
|
|
|
func (c *redisUploadCache) Touch(filePath string) {
|
|
err := c.client.Expire(context.Background(), c.filePathKey(filePath), uploadCacheTTL).Err()
|
|
if err != nil {
|
|
log.Printf("failed to touch upload in redis cache: %v", err)
|
|
}
|
|
}
|
|
|
|
func (c *redisUploadCache) Close() {
|
|
c.client.Close()
|
|
}
|