security: remediate vulnerabilities and issues from codebase audit
All checks were successful
Automated Container Build / build-and-push (push) Successful in 1m15s

This commit is contained in:
Elijah 2026-05-23 10:04:08 -07:00
parent b60a09196a
commit fb3f3a3393
15 changed files with 465 additions and 208 deletions

View file

@ -7,14 +7,13 @@ import (
"mime"
"os"
"path/filepath"
"strings"
"time"
"git.elijahkuntz.com/Elijah/drive/config"
"git.elijahkuntz.com/Elijah/drive/database"
"git.elijahkuntz.com/Elijah/drive/middleware"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
"golang.org/x/crypto/bcrypt"
)
type ShareHandler struct {
@ -37,13 +36,20 @@ func (h *ShareHandler) CreateShare(c *fiber.Ctx) error {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request"})
}
// Validate path
fullPath := filepath.Join(h.Config.StorageDir, filepath.FromSlash(req.Path))
// Validate path using resolveSafe
fullPath, err := resolveSafe(h.Config.StorageDir, req.Path)
if err != nil {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "invalid path"})
}
info, err := os.Stat(fullPath)
if err != nil {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "file not found"})
}
// Clean path for DB
cleanPath := filepath.ToSlash(filepath.Clean(req.Path))
isDirInt := 0
if info.IsDir() {
isDirInt = 1
@ -52,10 +58,9 @@ func (h *ShareHandler) CreateShare(c *fiber.Ctx) error {
id := uuid.New().String()
var hash *string
if req.Password != "" {
hashed, err := bcrypt.GenerateFromPassword([]byte(req.Password), 10)
encoded, err := hashPassword(req.Password)
if err == nil {
s := string(hashed)
hash = &s
hash = &encoded
}
}
@ -73,7 +78,7 @@ func (h *ShareHandler) CreateShare(c *fiber.Ctx) error {
_, err = h.DB.Exec(`
INSERT INTO share_links (token, target_path, is_dir, password_hash, expires_at, max_downloads, download_count)
VALUES (?, ?, ?, ?, ?, ?, 0)
`, id, req.Path, isDirInt, hash, expiresAt, maxDownloads)
`, id, cleanPath, isDirInt, hash, expiresAt, maxDownloads)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to create share"})
@ -191,7 +196,7 @@ func (h *ShareHandler) GetPublicShare(c *fiber.Ctx) error {
if req.Password == "" {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"require_password": true})
}
if err := bcrypt.CompareHashAndPassword([]byte(*hash), []byte(req.Password)); err != nil {
if !verifyPassword(req.Password, *hash) {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "invalid password"})
}
}
@ -199,14 +204,14 @@ func (h *ShareHandler) GetPublicShare(c *fiber.Ctx) error {
// Send file info
targetRelPath := filepath.Clean(filepath.FromSlash(path))
if subPath := c.Query("path"); subPath != "" {
cleanSub := filepath.Clean(filepath.FromSlash(subPath))
if strings.HasPrefix(cleanSub, "..") || strings.HasPrefix(cleanSub, "/") || strings.HasPrefix(cleanSub, "\\") {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "invalid path"})
}
targetRelPath = filepath.Join(targetRelPath, cleanSub)
targetRelPath = filepath.Join(targetRelPath, filepath.Clean(filepath.FromSlash(subPath)))
}
fullPath, err := resolveSafe(h.Config.StorageDir, targetRelPath)
if err != nil {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "invalid path"})
}
fullPath := filepath.Join(h.Config.StorageDir, targetRelPath)
info, err := os.Stat(fullPath)
if err != nil {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "file no longer exists"})
@ -251,22 +256,61 @@ func (h *ShareHandler) GetPublicShare(c *fiber.Ctx) error {
return c.JSON(response)
}
// CreateShareToken generates a short-lived HMAC token for public share downloads
// POST /api/public/share/:id/token
func (h *ShareHandler) CreateShareToken(c *fiber.Ctx) error {
id := c.Params("id")
var req struct {
Password string `json:"password"`
}
c.BodyParser(&req)
var hash *string
err := h.DB.QueryRow(`SELECT password_hash FROM share_links WHERE token = ?`, id).Scan(&hash)
if err != nil {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "share not found"})
}
if hash != nil {
if req.Password == "" || !verifyPassword(req.Password, *hash) {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "invalid password"})
}
}
// Generate a short-lived download token with the share ID
token, err := middleware.GenerateDownloadToken(0, "public_share_"+id, h.Config.JWTSecret)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to generate token"})
}
return c.JSON(fiber.Map{"token": token})
}
// DownloadPublicShare serves the file.
// GET /api/public/download/:id
func (h *ShareHandler) DownloadPublicShare(c *fiber.Ctx) error {
id := c.Params("id")
pwd := c.Query("pwd")
// Instead of ?pwd=..., we require the ?token=...
tokenString := c.Query("token")
if tokenString == "" {
return c.Status(fiber.StatusUnauthorized).SendString("missing download token")
}
// Validate the JWT download token
if err := middleware.ValidateDownloadToken(tokenString, h.Config.JWTSecret, "public_share_"+id); err != nil {
return c.Status(fiber.StatusUnauthorized).SendString("invalid or expired download token")
}
var path string
var hash *string
var expiresAt *time.Time
var maxDownloads *int
var downloads int
err := h.DB.QueryRow(`
SELECT target_path, password_hash, expires_at, max_downloads, download_count
SELECT target_path, expires_at, max_downloads, download_count
FROM share_links WHERE token = ?
`, id).Scan(&path, &hash, &expiresAt, &maxDownloads, &downloads)
`, id).Scan(&path, &expiresAt, &maxDownloads, &downloads)
if err != nil {
return c.Status(fiber.StatusNotFound).SendString("not found")
@ -280,22 +324,16 @@ func (h *ShareHandler) DownloadPublicShare(c *fiber.Ctx) error {
return c.Status(fiber.StatusForbidden).SendString("limit reached")
}
if hash != nil {
if err := bcrypt.CompareHashAndPassword([]byte(*hash), []byte(pwd)); err != nil {
return c.Status(fiber.StatusUnauthorized).SendString("unauthorized")
}
}
targetRelPath := filepath.Clean(filepath.FromSlash(path))
if subPath := c.Query("path"); subPath != "" {
cleanSub := filepath.Clean(filepath.FromSlash(subPath))
if strings.HasPrefix(cleanSub, "..") || strings.HasPrefix(cleanSub, "/") || strings.HasPrefix(cleanSub, "\\") {
return c.Status(fiber.StatusForbidden).SendString("invalid path")
}
targetRelPath = filepath.Join(targetRelPath, cleanSub)
targetRelPath = filepath.Join(targetRelPath, filepath.Clean(filepath.FromSlash(subPath)))
}
fullPath, err := resolveSafe(h.Config.StorageDir, targetRelPath)
if err != nil {
return c.Status(fiber.StatusForbidden).SendString("invalid path")
}
fullPath := filepath.Join(h.Config.StorageDir, targetRelPath)
info, err := os.Stat(fullPath)
if err != nil {
return c.Status(fiber.StatusNotFound).SendString("not found")