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:
@@ -1,6 +1,10 @@
|
||||
package boxstore
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -59,3 +63,87 @@ func TestStartRetentionSkipsOneTimeDownload(t *testing.T) {
|
||||
t.Fatalf("expected one-time download box to avoid retention expiry, got %s", manifest.ExpiresAt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSafeBoxFilePathRejectsTraversal(t *testing.T) {
|
||||
restoreUploadRoot := UploadRoot()
|
||||
defer SetUploadRoot(restoreUploadRoot)
|
||||
SetUploadRoot(t.TempDir())
|
||||
|
||||
boxID := "0123456789abcdef0123456789abcdef"
|
||||
if _, ok := SafeBoxFilePath(boxID, "../outside.txt"); ok {
|
||||
t.Fatal("expected traversal to be rejected")
|
||||
}
|
||||
if _, ok := SafeBoxFilePath("../bad", "file.txt"); ok {
|
||||
t.Fatal("expected invalid box id to be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddFileToZipRejectsUnsafeManifestName(t *testing.T) {
|
||||
restoreUploadRoot := UploadRoot()
|
||||
defer SetUploadRoot(restoreUploadRoot)
|
||||
SetUploadRoot(t.TempDir())
|
||||
|
||||
var buffer bytes.Buffer
|
||||
zipWriter := zip.NewWriter(&buffer)
|
||||
if err := AddFileToZip(zipWriter, "0123456789abcdef0123456789abcdef", "../outside.txt"); err == nil {
|
||||
t.Fatal("expected unsafe zip filename to be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestListFilesSkipsSymlinks(t *testing.T) {
|
||||
restoreUploadRoot := UploadRoot()
|
||||
defer SetUploadRoot(restoreUploadRoot)
|
||||
SetUploadRoot(t.TempDir())
|
||||
|
||||
boxID := "0123456789abcdef0123456789abcdef"
|
||||
if err := os.MkdirAll(BoxPath(boxID), 0755); err != nil {
|
||||
t.Fatalf("MkdirAll returned error: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(BoxPath(boxID), "safe.txt"), []byte("safe"), 0644); err != nil {
|
||||
t.Fatalf("WriteFile returned error: %v", err)
|
||||
}
|
||||
if err := os.Symlink(filepath.Join(BoxPath(boxID), "safe.txt"), filepath.Join(BoxPath(boxID), "link.txt")); err != nil {
|
||||
t.Skipf("symlink unavailable: %v", err)
|
||||
}
|
||||
|
||||
files, err := ListFiles(boxID)
|
||||
if err != nil {
|
||||
t.Fatalf("ListFiles returned error: %v", err)
|
||||
}
|
||||
if len(files) != 1 || files[0].Name != "safe.txt" {
|
||||
t.Fatalf("expected only regular file, got %#v", files)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBoxPasswordUsesBcryptAndVerifiesLegacy(t *testing.T) {
|
||||
restoreUploadRoot := UploadRoot()
|
||||
defer SetUploadRoot(restoreUploadRoot)
|
||||
SetUploadRoot(t.TempDir())
|
||||
|
||||
boxID := "0123456789abcdef0123456789abcdef"
|
||||
if err := os.MkdirAll(BoxPath(boxID), 0755); err != nil {
|
||||
t.Fatalf("MkdirAll returned error: %v", err)
|
||||
}
|
||||
if _, err := CreateManifest(boxID, models.CreateBoxRequest{Password: "secret"}); err != nil {
|
||||
t.Fatalf("CreateManifest returned error: %v", err)
|
||||
}
|
||||
manifest, err := ReadManifest(boxID)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadManifest returned error: %v", err)
|
||||
}
|
||||
if manifest.PasswordHashAlg != "bcrypt" {
|
||||
t.Fatalf("expected bcrypt password hash, got %q", manifest.PasswordHashAlg)
|
||||
}
|
||||
if !VerifyPassword(manifest, "secret") {
|
||||
t.Fatal("expected bcrypt password to verify")
|
||||
}
|
||||
|
||||
legacy := models.BoxManifest{
|
||||
PasswordSalt: "salt",
|
||||
PasswordHash: legacyPasswordHash("salt", "secret"),
|
||||
AuthToken: "token",
|
||||
}
|
||||
if !VerifyPassword(legacy, "secret") {
|
||||
t.Fatal("expected legacy password hash to verify")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user