mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-28 11:16:37 +08:00
86 lines
2.4 KiB
Go
86 lines
2.4 KiB
Go
package service
|
|
|
|
import (
|
|
"errors"
|
|
"net"
|
|
"net/url"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// validateURL parses raw and ensures the scheme is http/https and the
|
|
// target host is not a private/loopback/link-local address (SSRF guard).
|
|
func (p *ImageProxy) validateURL(raw string) (*url.URL, error) {
|
|
if raw == "" {
|
|
return nil, errors.New("missing url")
|
|
}
|
|
u, err := url.Parse(raw)
|
|
if err != nil || u.Host == "" {
|
|
return nil, errors.New("invalid url")
|
|
}
|
|
scheme := strings.ToLower(u.Scheme)
|
|
if scheme != "http" && scheme != "https" {
|
|
return nil, errors.New("unsupported scheme")
|
|
}
|
|
if !p.isAllowedRemoteHost(u.Hostname()) && isPrivateHost(u.Hostname()) {
|
|
return nil, errors.New("requests to private/internal hosts are not allowed")
|
|
}
|
|
return u, nil
|
|
}
|
|
|
|
// isPrivateHost returns true only when host is a literal loopback, private,
|
|
// link-local or unspecified IP address. Hostnames are not resolved here because
|
|
// DNS poisoning can map public image CDNs to bogus private addresses.
|
|
func isPrivateHost(host string) bool {
|
|
if host == "" {
|
|
return true
|
|
}
|
|
if ip := net.ParseIP(host); ip != nil {
|
|
return isPrivateIP(ip)
|
|
}
|
|
return false
|
|
}
|
|
|
|
// isPrivateIP 判定单个 IP 是否属于回环/私网/链路本地/未指定地址。
|
|
func isPrivateIP(ip net.IP) bool {
|
|
return ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() ||
|
|
ip.IsLinkLocalMulticast() || ip.IsUnspecified()
|
|
}
|
|
|
|
// isAllowedLocalPath restricts local file reads to known-safe roots.
|
|
func (p *ImageProxy) isAllowedLocalPath(abs string) bool {
|
|
roots := []string{p.cfg.App.DataDir, p.cfg.Cache.CacheDir, p.cfg.Media.MoviesDir, p.cfg.Media.TVDir, p.cfg.Media.AnimeDir}
|
|
roots = append(roots, p.libraryRoots()...)
|
|
for _, root := range roots {
|
|
if strings.TrimSpace(root) == "" {
|
|
continue
|
|
}
|
|
rootAbs, err := filepath.Abs(root)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if strings.HasPrefix(abs, rootAbs+string(filepath.Separator)) || abs == rootAbs {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func isLocalImagePath(raw string) bool {
|
|
raw = strings.TrimSpace(raw)
|
|
if raw == "" || isHTTPish(raw) {
|
|
return false
|
|
}
|
|
ext := strings.ToLower(filepath.Ext(raw))
|
|
switch ext {
|
|
case ".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".tbn":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func isHTTPish(raw string) bool {
|
|
return strings.HasPrefix(strings.ToLower(raw), "http://") || strings.HasPrefix(strings.ToLower(raw), "https://")
|
|
}
|