From 9021007027e12e1161bf6901bb9d000adf176119 Mon Sep 17 00:00:00 2001 From: ShukeBta <272197458+ShukeBta@users.noreply.github.com> Date: Thu, 25 Jun 2026 02:33:30 +0800 Subject: [PATCH] fix: harden playback and security edge cases --- internal/handler/cloud_playback.go | 5 --- internal/handler/cloud_playback_test.go | 55 ++++++++++++++++++++++++ internal/service/ffmpeg_security.go | 14 +++++- internal/service/ffmpeg_security_test.go | 45 +++++++++++++++++++ web/public/artwork-cache-sw.js | 2 +- 5 files changed, 113 insertions(+), 8 deletions(-) create mode 100644 internal/handler/cloud_playback_test.go create mode 100644 internal/service/ffmpeg_security_test.go diff --git a/internal/handler/cloud_playback.go b/internal/handler/cloud_playback.go index 6080743..ad833f1 100644 --- a/internal/handler/cloud_playback.go +++ b/internal/handler/cloud_playback.go @@ -109,9 +109,6 @@ func proxyCloudResolvedLink(playback cloudPlaybackRequest) { clientMethod = http.MethodGet } upstreamMethod := clientMethod - if upstreamMethod == http.MethodHead { - upstreamMethod = http.MethodGet - } req, err := http.NewRequestWithContext(c.Request.Context(), upstreamMethod, playback.link.URL, nil) if err != nil { c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()}) @@ -122,8 +119,6 @@ func proxyCloudResolvedLink(playback cloudPlaybackRequest) { } if rng := c.GetHeader("Range"); rng != "" { req.Header.Set("Range", rng) - } else if clientMethod == http.MethodHead { - req.Header.Set("Range", "bytes=0-0") } if accept := c.GetHeader("Accept"); accept != "" { req.Header.Set("Accept", accept) diff --git a/internal/handler/cloud_playback_test.go b/internal/handler/cloud_playback_test.go new file mode 100644 index 0000000..d8c4a48 --- /dev/null +++ b/internal/handler/cloud_playback_test.go @@ -0,0 +1,55 @@ +package handler + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + + "github.com/ShukeBta/MediaStationGo/internal/service/cloud" +) + +func TestProxyCloudResolvedLinkUsesHEADWithoutSyntheticRange(t *testing.T) { + gin.SetMode(gin.TestMode) + var upstreamMethod, upstreamRange string + upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + upstreamMethod = r.Method + upstreamRange = r.Header.Get("Range") + w.Header().Set("Content-Type", "video/mp4") + w.Header().Set("Content-Length", "123456") + w.Header().Set("Accept-Ranges", "bytes") + w.WriteHeader(http.StatusOK) + })) + defer upstream.Close() + + rec := httptest.NewRecorder() + c, _ := gin.CreateTestContext(rec) + c.Request = httptest.NewRequest(http.MethodHead, "/api/cloud/play/openlist?ref=movie", nil) + + proxyCloudResolvedLink(cloudPlaybackRequest{ + c: c, + typ: "openlist", + ref: "movie", + link: &cloud.DirectLink{ + URL: upstream.URL + "/movie.mp4", + Proxy: true, + }, + }) + + if upstreamMethod != http.MethodHead { + t.Fatalf("upstream method = %q, want HEAD", upstreamMethod) + } + if upstreamRange != "" { + t.Fatalf("upstream Range = %q, want empty", upstreamRange) + } + if rec.Code != http.StatusOK { + t.Fatalf("status = %d, want 200", rec.Code) + } + if got := rec.Header().Get("Content-Length"); got != "123456" { + t.Fatalf("Content-Length = %q, want full upstream length", got) + } + if rec.Body.Len() != 0 { + t.Fatalf("HEAD response body length = %d, want 0", rec.Body.Len()) + } +} diff --git a/internal/service/ffmpeg_security.go b/internal/service/ffmpeg_security.go index 3b5e7a0..edfad1f 100644 --- a/internal/service/ffmpeg_security.go +++ b/internal/service/ffmpeg_security.go @@ -38,9 +38,9 @@ func EvaluateFFmpegSecurity(versionLine string) FFmpegSecurityStatus { FixedVersion: ffmpegPixelSmashFixedVersion, ParsedVersion: fmt.Sprintf("%d.%d.%d", major, minor, patch), } - if major == 8 && minor == 1 && patch < 2 { + if major == 8 && ffmpegVersionLess(major, minor, patch, 8, 1, 2) { status.Status = "vulnerable" - status.Message = "当前 FFmpeg 8.1.x 版本低于 8.1.2,可能受 PixelSmash 解码漏洞影响;请升级 ffmpeg/ffprobe" + status.Message = "当前 FFmpeg 8.x 版本低于 8.1.2,可能受 PixelSmash 解码漏洞影响;请升级 ffmpeg/ffprobe" return status } if major < 8 { @@ -50,6 +50,16 @@ func EvaluateFFmpegSecurity(versionLine string) FFmpegSecurityStatus { return status } +func ffmpegVersionLess(major, minor, patch, fixedMajor, fixedMinor, fixedPatch int) bool { + if major != fixedMajor { + return major < fixedMajor + } + if minor != fixedMinor { + return minor < fixedMinor + } + return patch < fixedPatch +} + func parseFFmpegVersionLine(versionLine string) (major, minor, patch int, ok bool) { match := ffmpegVersionRE.FindStringSubmatch(strings.TrimSpace(versionLine)) if len(match) < 3 { diff --git a/internal/service/ffmpeg_security_test.go b/internal/service/ffmpeg_security_test.go new file mode 100644 index 0000000..e43f0a5 --- /dev/null +++ b/internal/service/ffmpeg_security_test.go @@ -0,0 +1,45 @@ +package service + +import "testing" + +func TestEvaluateFFmpegSecurityClassifiesPixelSmashFixedVersion(t *testing.T) { + tests := []struct { + name string + versionLine string + want string + }{ + { + name: "ffmpeg 8.0 is vulnerable", + versionLine: "ffmpeg version 8.0 Copyright", + want: "vulnerable", + }, + { + name: "ffmpeg 8.1.1 is vulnerable", + versionLine: "ffmpeg version 8.1.1 Copyright", + want: "vulnerable", + }, + { + name: "ffmpeg 8.1.2 is ok", + versionLine: "ffmpeg version 8.1.2 Copyright", + want: "ok", + }, + { + name: "older major needs distro review", + versionLine: "ffmpeg version 7.1.2 Copyright", + want: "review", + }, + { + name: "unrecognized is unknown", + versionLine: "not ffmpeg", + want: "unknown", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := EvaluateFFmpegSecurity(tt.versionLine); got.Status != tt.want { + t.Fatalf("status = %q, want %q (%+v)", got.Status, tt.want, got) + } + }) + } +} diff --git a/web/public/artwork-cache-sw.js b/web/public/artwork-cache-sw.js index e299e83..591691e 100644 --- a/web/public/artwork-cache-sw.js +++ b/web/public/artwork-cache-sw.js @@ -106,7 +106,7 @@ function artworkIdentity(url) { return `${url.origin}${url.pathname}?url=${url.searchParams.get('url') || ''}` } if (url.pathname.startsWith('/api/cloud/play/')) { - return `${url.origin}${url.pathname}` + return `${url.origin}${url.pathname}?ref=${url.searchParams.get('ref') || ''}` } return '' }