feat(security): use bcrypt hashes and safe paths for boxes

- Replace legacy salted password hashing with bcrypt and store hash alg
- Accept existing bcrypt hashes while keeping legacy verification fallback
- Validate box IDs and use SafeChildPath for box/file operations to prevent traversal
- Refactor download flow to share zip writer logic and correctly handle one-time deletes and optional renew-on-download only after a successful zip writefeat(security): use bcrypt hashes and safe paths for boxes

- Replace legacy salted password hashing with bcrypt and store hash alg
- Accept existing bcrypt hashes while keeping legacy verification fallback
- Validate box IDs and use SafeChildPath for box/file operations to prevent traversal
- Refactor download flow to share zip writer logic and correctly handle one-time deletes and optional renew-on-download only after a successful zip write
This commit is contained in:
2026-04-28 21:42:36 +03:00
parent a5d6d69be0
commit cb026d4fd1
15 changed files with 545 additions and 68 deletions

View File

@@ -14,8 +14,19 @@ func SafeFilename(name string) (string, bool) {
}
func SafeChildPath(parent string, filename string) (string, bool) {
path := filepath.Join(parent, filename)
return path, strings.HasPrefix(path, parent+string(filepath.Separator))
parent = filepath.Clean(parent)
filename = strings.TrimSpace(filename)
if parent == "" || filename == "" || filepath.IsAbs(filename) {
return "", false
}
path := filepath.Clean(filepath.Join(parent, filename))
relative, err := filepath.Rel(parent, path)
if err != nil || relative == "." || strings.HasPrefix(relative, ".."+string(filepath.Separator)) || relative == ".." {
return "", false
}
return path, true
}
func UniqueFilename(directory string, filename string) string {

20
lib/helpers/paths_test.go Normal file
View File

@@ -0,0 +1,20 @@
package helpers
import (
"path/filepath"
"testing"
)
func TestSafeChildPathRejectsTraversalAndAbsolutePaths(t *testing.T) {
parent := filepath.Join(t.TempDir(), "parent")
if _, ok := SafeChildPath(parent, "../outside.txt"); ok {
t.Fatal("expected traversal to be rejected")
}
if _, ok := SafeChildPath(parent, filepath.Join(string(filepath.Separator), "tmp", "outside.txt")); ok {
t.Fatal("expected absolute path to be rejected")
}
if path, ok := SafeChildPath(parent, "inside.txt"); !ok || path != filepath.Join(parent, "inside.txt") {
t.Fatalf("expected safe child path, got path=%q ok=%v", path, ok)
}
}