From 2d4a34c8413b85782f9c1ac6844fc9915845d615 Mon Sep 17 00:00:00 2001 From: Pavel Pikta Date: Fri, 18 Sep 2026 21:11:59 +0300 Subject: [PATCH] refactor: image URL validation Signed-off-by: Pavel Pikta --- server/torr/dbwrapper.go | 14 ++++- server/torr/utils/webImageChecker.go | 30 ++++++---- server/torr/utils/webImageChecker_test.go | 67 +++++++++++++++++++++++ 3 files changed, 98 insertions(+), 13 deletions(-) create mode 100644 server/torr/utils/webImageChecker_test.go diff --git a/server/torr/dbwrapper.go b/server/torr/dbwrapper.go index 3a2080a42..ee29beef2 100644 --- a/server/torr/dbwrapper.go +++ b/server/torr/dbwrapper.go @@ -33,8 +33,18 @@ func AddTorrentDB(torr *Torrent) { t.Data = torr.Data } - if torr.Poster != "" && utils.CheckImgUrl(torr.Poster) { - t.Poster = torr.Poster + t.Poster = torr.Poster + if t.Poster != "" { + var existing string + if db := GetTorrentDB(torr.Hash()); db != nil { + existing = db.Poster + } + if existing != t.Poster { + ok, verified := utils.CheckImgUrl(t.Poster) + if !ok || (existing != "" && !verified) { + t.Poster = existing + } + } } t.Size = torr.Size if t.Size == 0 && torr.Torrent != nil { diff --git a/server/torr/utils/webImageChecker.go b/server/torr/utils/webImageChecker.go index 66bbec500..2ee8ba674 100644 --- a/server/torr/utils/webImageChecker.go +++ b/server/torr/utils/webImageChecker.go @@ -8,6 +8,7 @@ import ( _ "image/png" "io" "net/http" + "net/url" "strings" "time" @@ -16,9 +17,17 @@ import ( "server/log" ) -func CheckImgUrl(link string) bool { +// CheckImgUrl reports whether link may be stored. +// verified is true only when a real image body was decoded; a timeout or +// transport error is ok but not verified, so a caller with an existing poster +// should keep it. +func CheckImgUrl(link string) (ok, verified bool) { if link == "" { - return false + return false, false + } + u, err := url.Parse(link) + if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" { + return false, false } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) @@ -27,17 +36,13 @@ func CheckImgUrl(link string) bool { req, err := http.NewRequestWithContext(ctx, "GET", link, nil) if err != nil { log.TLogln("Error create request for image:", err) - return false - } - - client := &http.Client{ - Timeout: 5 * time.Second, + return false, false } - resp, err := client.Do(req) + resp, err := http.DefaultClient.Do(req) if err != nil { log.TLogln("Error check image:", err) - return false + return true, false } defer resp.Body.Close() @@ -50,7 +55,10 @@ func CheckImgUrl(link string) bool { } if err != nil { log.TLogln("Error decode image:", err) - return false + if ctx.Err() != nil { + return true, false + } + return false, false } - return true + return true, true } diff --git a/server/torr/utils/webImageChecker_test.go b/server/torr/utils/webImageChecker_test.go new file mode 100644 index 000000000..db889e4d9 --- /dev/null +++ b/server/torr/utils/webImageChecker_test.go @@ -0,0 +1,67 @@ +package utils + +import ( + "bytes" + "image" + "image/jpeg" + "net/http" + "net/http/httptest" + "testing" +) + +func TestCheckImgUrl(t *testing.T) { + for _, link := range []string{ + "", + "javascript:alert(1)", + "data:text/html,", + "file:///etc/passwd", + } { + if ok, _ := CheckImgUrl(link); ok { + t.Errorf("%q: want reject", link) + } + } + + var jpegBuf bytes.Buffer + if err := jpeg.Encode(&jpegBuf, image.NewRGBA(image.Rect(0, 0, 1, 1)), nil); err != nil { + t.Fatal(err) + } + + okSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "image/jpeg") + w.Write(jpegBuf.Bytes()) + })) + defer okSrv.Close() + if ok, verified := CheckImgUrl(okSrv.URL + "/poster.jpg"); !ok || !verified { + t.Error("jpeg: want ok and verified") + } + + htmlSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "text/html") + w.Write([]byte("not an image")) + })) + defer htmlSrv.Close() + if ok, _ := CheckImgUrl(htmlSrv.URL + "/index.html"); ok { + t.Error("html: want reject") + } + + hangSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + <-r.Context().Done() + })) + defer hangSrv.Close() + if ok, verified := CheckImgUrl(hangSrv.URL + "/poster.jpg"); !ok || verified { + t.Error("timeout: want ok, not verified") + } + + slowSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "image/jpeg") + w.WriteHeader(http.StatusOK) + if f, ok := w.(http.Flusher); ok { + f.Flush() + } + <-r.Context().Done() + })) + defer slowSrv.Close() + if ok, verified := CheckImgUrl(slowSrv.URL + "/poster.jpg"); !ok || verified { + t.Error("decode after deadline: want ok, not verified") + } +}