Files
xTeVe/src/security.go
T
nathan 3bbea8e952 Phase 2b: server/client timeouts, atomic state writes, log mutex, panic and shutdown fixes
- http.Server with ReadHeaderTimeout/IdleTimeout (no read/write timeouts: /stream/ is long-lived)
- shared outbound clients: providerHTTPClient (5m), apiHTTPClient (30s), imgcache client (30s)
- imgcache: download outside the lock, per-item helper, cache URL uses the file name not the fs path
- writeFileAtomic (temp + fsync + rename) for settings/xepg/pms/urls/authentication JSON
- one package-level logMu for WebScreenLog and notifications; ring buffer keeps the newest lines
- notifications evict the oldest instead of random map entries
- xepg XMLTV file removal rebuilt after the loop; data.go range-mutation removed
- API handler returns after error body; WS request/response fresh per command
- checked type assertions in data/backup/provider/screen
- SIGINT/SIGTERM handled in main via src.Shutdown(); fatal paths exit 1
2026-09-26 13:17:07 +10:00

131 lines
3.7 KiB
Go

package src
import (
"errors"
"fmt"
"net/url"
"os"
"path/filepath"
"regexp"
"strings"
)
// Input validation for values that reach the file system or an external
// process. Everything here is deliberately strict: xTeVe runs on a trusted
// LAN, but playlists, EPG feeds and any browser on that LAN are not trusted.
var uploadExtensions = map[string]bool{".png": true, ".jpg": true, ".jpeg": true, ".gif": true, ".webp": true, ".ico": true}
var unsafeFilenameChars = regexp.MustCompile(`[^A-Za-z0-9._-]+`)
// sanitizeUploadFilename : reduces a client-supplied logo file name to a safe
// base name with an image extension.
func sanitizeUploadFilename(name string) (string, error) {
name = strings.TrimSpace(name)
// Browsers on Windows may send backslash paths; keep only the last part.
if i := strings.LastIndexAny(name, `\/`); i >= 0 {
name = name[i+1:]
}
name = filepath.Base(name)
if name == "" || name == "." || name == ".." || name == string(filepath.Separator) {
return "", errors.New("invalid file name")
}
var ext = strings.ToLower(filepath.Ext(name))
if !uploadExtensions[ext] {
return "", fmt.Errorf("unsupported image type %q", ext)
}
var base = unsafeFilenameChars.ReplaceAllString(strings.TrimSuffix(name, filepath.Ext(name)), "_")
base = strings.Trim(base, "._")
if base == "" {
return "", errors.New("invalid file name")
}
return base + ext, nil
}
var streamingBinaries = map[string]bool{"ffmpeg": true, "ffmpeg.exe": true, "vlc": true, "vlc.exe": true, "cvlc": true, "cvlc.exe": true}
// checkStreamingBinary : the configured ffmpeg / VLC path must be an existing
// regular file whose name is one of the known players. This is what stops a
// settings change from turning the buffer into "run any program".
func checkStreamingBinary(path string) error {
if err := checkFile(path); err != nil {
return err
}
fi, err := os.Stat(getPlatformFile(path))
if err != nil {
return err
}
if !fi.Mode().IsRegular() {
return fmt.Errorf("%s is not a regular file", path)
}
if !streamingBinaries[strings.ToLower(filepath.Base(path))] {
return fmt.Errorf("%s is not a supported streaming binary (ffmpeg, vlc, cvlc)", path)
}
return nil
}
var streamSchemes = map[string]bool{"http": true, "https": true, "rtsp": true, "rtsps": true, "rtmp": true, "rtmps": true, "rtp": true, "udp": true, "mms": true, "mmsh": true}
// checkStreamURL : provider stream URLs are handed to ffmpeg / VLC verbatim.
// Only network schemes are allowed, so a playlist cannot point the buffer at
// file:, concat:, pipe: or similar local sources.
func checkStreamURL(raw string) error {
u, err := url.Parse(strings.TrimSpace(raw))
if err != nil {
return err
}
if !streamSchemes[strings.ToLower(u.Scheme)] {
return fmt.Errorf("stream URL scheme %q is not allowed", u.Scheme)
}
if u.Host == "" {
return errors.New("stream URL has no host")
}
return nil
}
// insideDir : true when path (already joined) stays inside dir.
func insideDir(dir, path string) bool {
dir = filepath.Clean(dir)
path = filepath.Clean(path)
if path == dir {
return true
}
return strings.HasPrefix(path, dir+string(filepath.Separator))
}
// plexTokenMask : what the UI sees instead of the real Plex token.
const plexTokenMask = "********"
// maskSettings : copy of Settings with secrets replaced for the web client.
func maskSettings(s SettingsStruct) SettingsStruct {
if len(s.PlexToken) > 0 {
s.PlexToken = plexTokenMask
}
return s
}
// writePrivateFile : like writeByteToFile but readable only by the owner.
func writePrivateFile(file string, data []byte) error {
// The temp file is chmod'ed before the rename, so an existing
// world-readable file is replaced by a 0600 one.
return writeFileAtomic(getPlatformFile(file), data, 0600)
}