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

@ -293,7 +293,7 @@ func (h *FSHandler) GetExtendedFileInfo(c *fiber.Ctx) error {
// For files, attempt to get media metadata
cat := categorizeFile(filepath.Ext(info.Name()))
if cat == "images" || cat == "videos" || cat == "audio" {
cmd := exec.Command("exiftool", "-json", resolvedPath)
cmd := exec.Command("exiftool", "-json", "--", resolvedPath)
output, err := cmd.Output()
if err == nil {
var meta []map[string]interface{}
@ -327,7 +327,7 @@ func (h *FSHandler) CreateFolder(c *fiber.Ctx) error {
parentDir := filepath.Dir(body.Path)
folderName := filepath.Base(body.Path)
parentFullPath, err := h.resolveSafe(parentDir)
parentFullPath, err := resolveSafe(h.Config.StorageDir, parentDir)
if err != nil {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": err.Error()})
}
@ -371,7 +371,7 @@ func (h *FSHandler) Rename(c *fiber.Ctx) error {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
}
oldFull, err := h.resolveSafe(body.OldPath)
oldFull, err := resolveSafe(h.Config.StorageDir, body.OldPath)
if err != nil {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": err.Error()})
}
@ -380,7 +380,7 @@ func (h *FSHandler) Rename(c *fiber.Ctx) error {
uniqueName := GetUniquePath(parentDir, body.NewName)
newPath := filepath.Join(filepath.Dir(body.OldPath), uniqueName)
newFull, err := h.resolveSafe(newPath)
newFull, err := resolveSafe(h.Config.StorageDir, newPath)
if err != nil {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": err.Error()})
}
@ -414,12 +414,12 @@ func (h *FSHandler) Move(c *fiber.Ctx) error {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
}
srcFull, err := h.resolveSafe(body.SourcePath)
srcFull, err := resolveSafe(h.Config.StorageDir, body.SourcePath)
if err != nil {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": err.Error()})
}
dstFull, err := h.resolveSafe(body.DestPath)
dstFull, err := resolveSafe(h.Config.StorageDir, body.DestPath)
if err != nil {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": err.Error()})
}
@ -468,12 +468,12 @@ func (h *FSHandler) Copy(c *fiber.Ctx) error {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
}
srcFull, err := h.resolveSafe(body.SourcePath)
srcFull, err := resolveSafe(h.Config.StorageDir, body.SourcePath)
if err != nil {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": err.Error()})
}
dstFull, err := h.resolveSafe(body.DestPath)
dstFull, err := resolveSafe(h.Config.StorageDir, body.DestPath)
if err != nil {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": err.Error()})
}
@ -643,7 +643,7 @@ func (h *FSHandler) DownloadFolder(c *fiber.Ctx) error {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "path is required"})
}
fullPath, err := h.resolveSafe(folderPath)
fullPath, err := resolveSafe(h.Config.StorageDir, folderPath)
if err != nil {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": err.Error()})
}
@ -722,7 +722,7 @@ func (h *FSHandler) DownloadBulk(c *fiber.Ctx) error {
defer zipWriter.Close()
for _, p := range body.Paths {
fullPath, err := h.resolveSafe(p)
fullPath, err := resolveSafe(h.Config.StorageDir, p)
if err != nil {
continue
}
@ -851,8 +851,26 @@ func (h *FSHandler) Search(c *fiber.Ctx) error {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "query parameter 'q' is required"})
}
// Append wildcard for prefix matching
ftsQuery := query + "*"
// Sanitize FTS5 query to prevent syntax errors
words := strings.Fields(query)
var safeWords []string
for _, w := range words {
// Keep only alphanumeric characters
var clean strings.Builder
for _, r := range w {
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') {
clean.WriteRune(r)
}
}
if clean.Len() > 0 {
safeWords = append(safeWords, clean.String()+"*")
}
}
ftsQuery := strings.Join(safeWords, " AND ")
if ftsQuery == "" {
return c.JSON(fiber.Map{"items": []FileInfo{}, "count": 0})
}
rows, err := h.DB.Query(`
SELECT f.path, f.name, f.is_dir, f.size, f.mime_type, f.is_pinned
@ -918,7 +936,7 @@ func (h *FSHandler) Upload(c *fiber.Ctx) error {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "file is required"})
}
fullDest, err := h.resolveSafe(destPath)
fullDest, err := resolveSafe(h.Config.StorageDir, destPath)
if err != nil {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": err.Error()})
}
@ -1034,26 +1052,6 @@ func (h *FSHandler) StorageDashboard(c *fiber.Ctx) error {
// --- Helper functions ---
func (h *FSHandler) resolveSafe(relativePath string) (string, error) {
cleaned := filepath.Clean(relativePath)
if strings.Contains(cleaned, "..") {
return "", fmt.Errorf("path traversal detected")
}
full := filepath.Join(h.Config.StorageDir, cleaned)
abs, err := filepath.Abs(full)
if err != nil {
return "", fmt.Errorf("invalid path")
}
absRoot, _ := filepath.Abs(h.Config.StorageDir)
if abs != absRoot && !strings.HasPrefix(abs, absRoot+string(filepath.Separator)) {
return "", fmt.Errorf("access denied")
}
return abs, nil
}
func generateChecksum(filePath string) (string, error) {
f, err := os.Open(filePath)
if err != nil {
@ -1132,6 +1130,18 @@ func (h *FSHandler) ServeThumbnail(c *fiber.Ctx) error {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "missing checksum"})
}
// Sanitize checksum: should only contain hex characters to prevent path traversal
isHex := true
for _, char := range checksum {
if !((char >= '0' && char <= '9') || (char >= 'a' && char <= 'f') || (char >= 'A' && char <= 'F')) {
isHex = false
break
}
}
if !isHex {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid checksum format"})
}
thumbPath := filepath.Join(h.Config.ThumbnailDir, checksum+".jpg")
if _, err := os.Stat(thumbPath); os.IsNotExist(err) {