security: remediate vulnerabilities and issues from codebase audit
All checks were successful
Automated Container Build / build-and-push (push) Successful in 1m15s
All checks were successful
Automated Container Build / build-and-push (push) Successful in 1m15s
This commit is contained in:
parent
b60a09196a
commit
fb3f3a3393
15 changed files with 465 additions and 208 deletions
|
|
@ -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")
|
||||
|
|
|
|||
Reference in a new issue