mirror of
https://github.com/sbondCo/Watcharr.git
synced 2026-08-07 07:14:44 +00:00
fix(plex): strip X-Plex-Token on cross-host redirect
The Plex integration used bare http.Client values with no CheckRedirect policy, so they followed redirects automatically. net/http strips the standard sensitive headers on a cross-host redirect but not custom-named headers, so the Plex token carried in X-Plex-Token was forwarded verbatim to any host the configured PLEX_HOST redirected to, disclosing the credential. Route all Plex outbound calls through a shared client whose CheckRedirect policy deletes X-Plex-Token when the redirect target host differs from the original request host. Includes a regression test. Signed-off-by: tonghuaroot <tonghuaroot@gmail.com>
This commit is contained in:
@@ -334,6 +334,24 @@ type PlexClientResources []struct {
|
||||
} `json:"connections"`
|
||||
}
|
||||
|
||||
// plexHTTPClient is the shared client for all Plex outbound calls. Its
|
||||
// CheckRedirect policy strips the custom X-Plex-Token header when a redirect
|
||||
// crosses to a different host. net/http already strips the standard sensitive
|
||||
// headers (Authorization, Cookie, WWW-Authenticate) on a cross-host redirect,
|
||||
// but it does NOT strip custom-named headers, so without this the Plex token
|
||||
// would be forwarded to any host the configured PLEX_HOST redirects to.
|
||||
var plexHTTPClient = &http.Client{
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
if len(via) == 0 {
|
||||
return nil
|
||||
}
|
||||
if req.URL.Host != via[0].URL.Host {
|
||||
req.Header.Del("X-Plex-Token")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
cfg *config.ServerConfig
|
||||
}
|
||||
@@ -345,7 +363,7 @@ func NewService(cfg *config.ServerConfig) *Service {
|
||||
}
|
||||
|
||||
func (s *Service) GetPlexIdentity(host string) (PlexIdentity, error) {
|
||||
httpClient := &http.Client{}
|
||||
httpClient := plexHTTPClient
|
||||
req, err := http.NewRequest("GET", host+"/identity", nil)
|
||||
if err != nil {
|
||||
return PlexIdentity{}, err
|
||||
@@ -369,7 +387,7 @@ func (s *Service) GetPlexIdentity(host string) (PlexIdentity, error) {
|
||||
}
|
||||
|
||||
func (s *Service) FetchPlexAccountFromToken(token string) (PlexUser, error) {
|
||||
httpClient := &http.Client{}
|
||||
httpClient := plexHTTPClient
|
||||
req, err := http.NewRequest("GET", "https://plex.tv/users/account.json", nil)
|
||||
if err != nil {
|
||||
return PlexUser{}, err
|
||||
@@ -419,7 +437,7 @@ func (s *Service) UpdateConfigPlexHost(cfg *config.ServerConfig, v string) (Plex
|
||||
}
|
||||
|
||||
func (s *Service) GetPlexLibraries(plexAuth string) (PlexLibrariesResponse, error) {
|
||||
httpClient := &http.Client{}
|
||||
httpClient := plexHTTPClient
|
||||
req, err := http.NewRequest("GET", s.cfg.PLEX_HOST+"/library/sections", nil)
|
||||
if err != nil {
|
||||
return PlexLibrariesResponse{}, err
|
||||
@@ -444,7 +462,7 @@ func (s *Service) GetPlexLibraries(plexAuth string) (PlexLibrariesResponse, erro
|
||||
}
|
||||
|
||||
func (s *Service) GetPlexLibraryItems(plexAuth string, libraryKey string) (PlexLibraryItemsResponse, error) {
|
||||
httpClient := &http.Client{}
|
||||
httpClient := plexHTTPClient
|
||||
req, err := http.NewRequest("GET", s.cfg.PLEX_HOST+"/library/sections/"+libraryKey+"/all?includeGuids=1", nil)
|
||||
if err != nil {
|
||||
return PlexLibraryItemsResponse{}, err
|
||||
@@ -469,7 +487,7 @@ func (s *Service) GetPlexLibraryItems(plexAuth string, libraryKey string) (PlexL
|
||||
}
|
||||
|
||||
func (s *Service) GetPlexLibraryItemSeasons(plexAuth string, ratingKey string) (PlexLibraryItemSeasonsResponse, error) {
|
||||
httpClient := &http.Client{}
|
||||
httpClient := plexHTTPClient
|
||||
req, err := http.NewRequest("GET", s.cfg.PLEX_HOST+"/library/metadata/"+ratingKey+"/children", nil)
|
||||
if err != nil {
|
||||
return PlexLibraryItemSeasonsResponse{}, err
|
||||
@@ -494,7 +512,7 @@ func (s *Service) GetPlexLibraryItemSeasons(plexAuth string, ratingKey string) (
|
||||
}
|
||||
|
||||
func (s *Service) GetPlexLibraryItemEpisodes(plexAuth string, ratingKey string) (PlexLibraryItemEpisodesResponse, error) {
|
||||
httpClient := &http.Client{}
|
||||
httpClient := plexHTTPClient
|
||||
req, err := http.NewRequest("GET", s.cfg.PLEX_HOST+"/library/metadata/"+ratingKey+"/allLeaves", nil)
|
||||
if err != nil {
|
||||
return PlexLibraryItemEpisodesResponse{}, err
|
||||
@@ -522,7 +540,7 @@ func (s *Service) GetPlexLibraryItemEpisodes(plexAuth string, ratingKey string)
|
||||
// so they can authenticate against it for api requests.
|
||||
// If no auth token is returned or errored, assume user doesn't have access to home plex server library.
|
||||
func (s *Service) GetPlexHomeServerAuthToken(plexAuth string, userClientId string) (string, error) {
|
||||
httpClient := &http.Client{}
|
||||
httpClient := plexHTTPClient
|
||||
req, err := http.NewRequest("GET", "https://clients.plex.tv/api/v2/resources", nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package plex
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/sbondCo/Watcharr/config"
|
||||
)
|
||||
|
||||
// TestGetPlexLibraries_StripsTokenOnCrossHostRedirect verifies the Plex client
|
||||
// does not forward X-Plex-Token across a cross-host redirect but preserves it
|
||||
// on a same-host redirect.
|
||||
func TestGetPlexLibraries_StripsTokenOnCrossHostRedirect(t *testing.T) {
|
||||
const token = "secret-plex-token"
|
||||
|
||||
t.Run("cross-host strips token", func(t *testing.T) {
|
||||
var finalToken string
|
||||
final := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
finalToken = r.Header.Get("X-Plex-Token")
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(`{"MediaContainer":{}}`))
|
||||
}))
|
||||
defer final.Close()
|
||||
redir := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, final.URL+r.URL.Path, http.StatusFound)
|
||||
}))
|
||||
defer redir.Close()
|
||||
|
||||
svc := NewService(&config.ServerConfig{PLEX_HOST: redir.URL})
|
||||
_, _ = svc.GetPlexLibraries(token)
|
||||
if finalToken != "" {
|
||||
t.Fatalf("X-Plex-Token forwarded cross-host = %q, want empty", finalToken)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("same-host keeps token", func(t *testing.T) {
|
||||
var finalToken string
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Query().Get("r") == "1" {
|
||||
finalToken = r.Header.Get("X-Plex-Token")
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(`{"MediaContainer":{}}`))
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, r.URL.Path+"?r=1", http.StatusFound)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
svc := NewService(&config.ServerConfig{PLEX_HOST: srv.URL})
|
||||
_, _ = svc.GetPlexLibraries(token)
|
||||
if finalToken != token {
|
||||
t.Fatalf("X-Plex-Token on same host = %q, want %q", finalToken, token)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user