From e97891175a1782c3fc99c8379407068e3826da94 Mon Sep 17 00:00:00 2001 From: soldosluka857 Date: Sat, 30 May 2026 02:39:11 +0000 Subject: [PATCH] fix: security hardening and HTTP status code corrections - importSTRMHandler: add URL scheme validation (blocks file://, ftp://, etc.) - backup Delete/Restore: harden path traversal check (block backslash, require .db extension) - HTTP 201 for create endpoints: register, subscription, download client, notify channel, library, STRM import - Error handling: return 500 for service/infra errors in download client and notify channel handlers Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- internal/handler/auth.go | 2 +- internal/handler/download_clients.go | 6 +++--- internal/handler/media.go | 2 +- internal/handler/notify_channels.go | 6 +++--- internal/handler/strm.go | 13 +++++++++---- internal/handler/subscriptions.go | 2 +- internal/service/backup.go | 18 ++++++++++++++++-- 7 files changed, 34 insertions(+), 15 deletions(-) diff --git a/internal/handler/auth.go b/internal/handler/auth.go index 7175c59..9e85fcc 100644 --- a/internal/handler/auth.go +++ b/internal/handler/auth.go @@ -65,7 +65,7 @@ func registerHandler(svc *service.Container) gin.HandlerFunc { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - c.JSON(http.StatusOK, gin.H{ + c.JSON(http.StatusCreated, gin.H{ "user": u, "tokens": tokens, }) diff --git a/internal/handler/download_clients.go b/internal/handler/download_clients.go index 00a03f4..7c05286 100644 --- a/internal/handler/download_clients.go +++ b/internal/handler/download_clients.go @@ -34,13 +34,13 @@ func createDownloadClientHandler(svc *service.Container) gin.HandlerFunc { } row, err := svc.DownloadClients.Create(c.Request.Context(), in) if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // 让真正发起下载的 DownloadService 立刻读到新的 qb 配置, // 避免保存后还要重启进程才能生效。 _ = svc.Downloads.ReloadConfig(c.Request.Context()) - c.JSON(http.StatusOK, row) + c.JSON(http.StatusCreated, row) } } @@ -53,7 +53,7 @@ func updateDownloadClientHandler(svc *service.Container) gin.HandlerFunc { } row, err := svc.DownloadClients.Update(c.Request.Context(), c.Param("id"), in) if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } _ = svc.Downloads.ReloadConfig(c.Request.Context()) diff --git a/internal/handler/media.go b/internal/handler/media.go index bfa71e2..e64a344 100644 --- a/internal/handler/media.go +++ b/internal/handler/media.go @@ -58,7 +58,7 @@ func createLibraryHandler(svc *service.Container) gin.HandlerFunc { svc.Audit.Record(c.Request.Context(), toString(uid), "library.create", l.ID, c.ClientIP(), l.Path) // Refresh fsnotify watcher to pick up the new library root. go func() { _ = svc.Watcher.Refresh(context.Background()) }() - c.JSON(http.StatusOK, l) + c.JSON(http.StatusCreated, l) } } diff --git a/internal/handler/notify_channels.go b/internal/handler/notify_channels.go index bacbfb1..1f820fb 100644 --- a/internal/handler/notify_channels.go +++ b/internal/handler/notify_channels.go @@ -33,10 +33,10 @@ func createNotifyChannelHandler(svc *service.Container) gin.HandlerFunc { } row, err := svc.NotifyChannels.Create(c.Request.Context(), in) if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } - c.JSON(http.StatusOK, row) + c.JSON(http.StatusCreated, row) } } @@ -49,7 +49,7 @@ func updateNotifyChannelHandler(svc *service.Container) gin.HandlerFunc { } row, err := svc.NotifyChannels.Update(c.Request.Context(), c.Param("id"), in) if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, row) diff --git a/internal/handler/strm.go b/internal/handler/strm.go index 9bfb445..81554c8 100644 --- a/internal/handler/strm.go +++ b/internal/handler/strm.go @@ -77,17 +77,22 @@ func importSTRMHandler(svc *service.Container) gin.HandlerFunc { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } + url := strings.TrimSpace(req.URL) + if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") { + c.JSON(http.StatusBadRequest, gin.H{"error": "url must start with http:// or https://"}) + return + } m := &model.Media{ LibraryID: req.LibraryID, Title: req.Title, - Path: req.URL, // unique-index target — keep it identical to the URL - STRMURL: req.URL, + Path: url, + STRMURL: url, Container: "strm", } if err := svc.Repo.Media.Upsert(c.Request.Context(), m); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } - c.JSON(http.StatusOK, m) + c.JSON(http.StatusCreated, m) } } diff --git a/internal/handler/subscriptions.go b/internal/handler/subscriptions.go index 7356877..7747608 100644 --- a/internal/handler/subscriptions.go +++ b/internal/handler/subscriptions.go @@ -81,7 +81,7 @@ func createSubscriptionHandler(svc *service.Container) gin.HandlerFunc { enriched := []model.Subscription{*s} service.EnrichSubscriptionProgress(c.Request.Context(), svc.Repo, enriched) *s = enriched[0] - c.JSON(http.StatusOK, s) + c.JSON(http.StatusCreated, s) } } diff --git a/internal/service/backup.go b/internal/service/backup.go index 881202c..45e3b35 100644 --- a/internal/service/backup.go +++ b/internal/service/backup.go @@ -106,7 +106,7 @@ func (b *BackupService) List() ([]BackupInfo, error) { // Delete removes a single backup file. func (b *BackupService) Delete(filename string) error { - if strings.Contains(filename, "/") || strings.Contains(filename, "..") { + if !isValidBackupFilename(filename) { return errors.New("invalid filename") } path := filepath.Join(b.backupDir(), filename) @@ -117,7 +117,7 @@ func (b *BackupService) Delete(filename string) error { // reverse. WARNING: this is destructive — the live DB will be replaced. // Callers should shut down the server after this call. func (b *BackupService) Restore(ctx context.Context, filename string) error { - if strings.Contains(filename, "/") || strings.Contains(filename, "..") { + if !isValidBackupFilename(filename) { return errors.New("invalid filename") } src := filepath.Join(b.backupDir(), filename) @@ -148,3 +148,17 @@ func (b *BackupService) Restore(ctx context.Context, filename string) error { zap.String("backup", filename)) return nil } + +// isValidBackupFilename rejects path traversal attempts and non-.db files. +func isValidBackupFilename(name string) bool { + if name == "" { + return false + } + if strings.ContainsAny(name, "/\\") || strings.Contains(name, "..") { + return false + } + if !strings.HasSuffix(name, ".db") { + return false + } + return true +}