chore: the updateConfigs api also adds a check for SAFE_PATHS

This commit is contained in:
wwqgtxx 2025-05-12 11:24:56 +08:00
parent a4fcd3af07
commit 2116640886
8 changed files with 43 additions and 22 deletions

View File

@ -138,7 +138,7 @@ func NewSsh(option SshOption) (*Ssh, error) {
} else { } else {
path := C.Path.Resolve(option.PrivateKey) path := C.Path.Resolve(option.PrivateKey)
if !C.Path.IsSafePath(path) { if !C.Path.IsSafePath(path) {
return nil, fmt.Errorf("path is not subpath of home directory: %s", path) return nil, C.Path.ErrNotSafePath(path)
} }
b, err = os.ReadFile(path) b, err = os.ReadFile(path)
if err != nil { if err != nil {

View File

@ -17,7 +17,6 @@ import (
var ( var (
errVehicleType = errors.New("unsupport vehicle type") errVehicleType = errors.New("unsupport vehicle type")
errSubPath = errors.New("path is not subpath of home directory")
) )
type healthCheckSchema struct { type healthCheckSchema struct {
@ -115,7 +114,7 @@ func ParseProxyProvider(name string, mapping map[string]any) (types.ProxyProvide
if schema.Path != "" { if schema.Path != "" {
path = C.Path.Resolve(schema.Path) path = C.Path.Resolve(schema.Path)
if !C.Path.IsSafePath(path) { if !C.Path.IsSafePath(path) {
return nil, fmt.Errorf("%w: %s", errSubPath, path) return nil, C.Path.ErrNotSafePath(path)
} }
} }
vehicle = resource.NewHTTPVehicle(schema.URL, path, schema.Proxy, schema.Header, resource.DefaultHttpTimeout, schema.SizeLimit) vehicle = resource.NewHTTPVehicle(schema.URL, path, schema.Proxy, schema.Header, resource.DefaultHttpTimeout, schema.SizeLimit)

View File

@ -83,7 +83,7 @@ func GetCertPool(customCA string, customCAString string) (*x509.CertPool, error)
if len(customCA) > 0 { if len(customCA) > 0 {
path := C.Path.Resolve(customCA) path := C.Path.Resolve(customCA)
if !C.Path.IsSafePath(path) { if !C.Path.IsSafePath(path) {
return nil, fmt.Errorf("path is not subpath of home directory: %s", path) return nil, C.Path.ErrNotSafePath(path)
} }
certificate, err = os.ReadFile(path) certificate, err = os.ReadFile(path)
if err != nil { if err != nil {

View File

@ -17,6 +17,7 @@ import (
type Path interface { type Path interface {
Resolve(path string) string Resolve(path string) string
IsSafePath(path string) bool IsSafePath(path string) bool
ErrNotSafePath(path string) error
} }
// LoadTLSKeyPair loads a TLS key pair from the provided certificate and private key data or file paths, supporting fallback resolution. // LoadTLSKeyPair loads a TLS key pair from the provided certificate and private key data or file paths, supporting fallback resolution.
@ -42,10 +43,12 @@ func LoadTLSKeyPair(certificate, privateKey string, path Path) (tls.Certificate,
certificate = path.Resolve(certificate) certificate = path.Resolve(certificate)
privateKey = path.Resolve(privateKey) privateKey = path.Resolve(privateKey)
var loadErr error var loadErr error
if path.IsSafePath(certificate) && path.IsSafePath(privateKey) { if !path.IsSafePath(certificate) {
cert, loadErr = tls.LoadX509KeyPair(certificate, privateKey) loadErr = path.ErrNotSafePath(certificate)
} else if !path.IsSafePath(privateKey) {
loadErr = path.ErrNotSafePath(privateKey)
} else { } else {
loadErr = fmt.Errorf("path is not subpath of home directory") cert, loadErr = tls.LoadX509KeyPair(certificate, privateKey)
} }
if loadErr != nil { if loadErr != nil {
return tls.Certificate{}, fmt.Errorf("parse certificate failed, maybe format error:%s, or path error: %s", painTextErr.Error(), loadErr.Error()) return tls.Certificate{}, fmt.Errorf("parse certificate failed, maybe format error:%s, or path error: %s", painTextErr.Error(), loadErr.Error())

View File

@ -755,7 +755,7 @@ func parseGeneral(cfg *RawConfig) (*General, error) {
func parseController(cfg *RawConfig) (*Controller, error) { func parseController(cfg *RawConfig) (*Controller, error) {
if path := cfg.ExternalUI; path != "" && !C.Path.IsSafePath(path) { if path := cfg.ExternalUI; path != "" && !C.Path.IsSafePath(path) {
return nil, fmt.Errorf("path is not subpath of home directory: %s", path) return nil, C.Path.ErrNotSafePath(path)
} }
return &Controller{ return &Controller{
ExternalController: cfg.ExternalController, ExternalController: cfg.ExternalController,

View File

@ -1,6 +1,7 @@
package constant package constant
import ( import (
"fmt"
"os" "os"
P "path" P "path"
"path/filepath" "path/filepath"
@ -87,10 +88,8 @@ func (p *path) IsSafePath(path string) bool {
if p.allowUnsafePath || features.CMFA { if p.allowUnsafePath || features.CMFA {
return true return true
} }
homedir := p.HomeDir()
path = p.Resolve(path) path = p.Resolve(path)
safePaths := append([]string{homedir}, p.safePaths...) // add homedir to safePaths for _, safePath := range p.SafePaths() {
for _, safePath := range safePaths {
if rel, err := filepath.Rel(safePath, path); err == nil { if rel, err := filepath.Rel(safePath, path); err == nil {
if filepath.IsLocal(rel) { if filepath.IsLocal(rel) {
return true return true
@ -100,6 +99,23 @@ func (p *path) IsSafePath(path string) bool {
return false return false
} }
func (p *path) SafePaths() []string {
return append([]string{p.homeDir}, p.safePaths...) // add homedir to safePaths
}
func (p *path) ErrNotSafePath(path string) error {
return ErrNotSafePath{Path: path, SafePaths: p.SafePaths()}
}
type ErrNotSafePath struct {
Path string
SafePaths []string
}
func (e ErrNotSafePath) Error() string {
return fmt.Sprintf("path is not subpath of home directory or SAFE_PATHS: %s \n allowed paths: %s", e.Path, e.SafePaths)
}
func (p *path) GetPathByHash(prefix, name string) string { func (p *path) GetPathByHash(prefix, name string) string {
hash := utils.MakeHash([]byte(name)) hash := utils.MakeHash([]byte(name))
filename := hash.String() filename := hash.String()

View File

@ -371,12 +371,20 @@ func updateConfigs(w http.ResponseWriter, r *http.Request) {
return return
} }
} else { } else {
if req.Path == "" { if req.Path == "" { // default path unneeded any safe check
req.Path = C.Path.Config() req.Path = C.Path.Config()
} else if !filepath.IsAbs(req.Path) { } else {
render.Status(r, http.StatusBadRequest) if !filepath.IsAbs(req.Path) {
render.JSON(w, r, newError("path is not a absolute path")) render.Status(r, http.StatusBadRequest)
return render.JSON(w, r, newError("path is not a absolute path"))
return
}
if !C.Path.IsSafePath(req.Path) {
render.Status(r, http.StatusBadRequest)
render.JSON(w, r, newError(C.Path.ErrNotSafePath(req.Path).Error()))
return
}
} }
cfg, err = executor.ParseWithPath(req.Path) cfg, err = executor.ParseWithPath(req.Path)

View File

@ -1,7 +1,6 @@
package provider package provider
import ( import (
"errors"
"fmt" "fmt"
"time" "time"
@ -12,10 +11,6 @@ import (
"github.com/metacubex/mihomo/rules/common" "github.com/metacubex/mihomo/rules/common"
) )
var (
errSubPath = errors.New("path is not subpath of home directory")
)
type ruleProviderSchema struct { type ruleProviderSchema struct {
Type string `provider:"type"` Type string `provider:"type"`
Behavior string `provider:"behavior"` Behavior string `provider:"behavior"`
@ -53,7 +48,7 @@ func ParseRuleProvider(name string, mapping map[string]any, parse common.ParseRu
if schema.Path != "" { if schema.Path != "" {
path = C.Path.Resolve(schema.Path) path = C.Path.Resolve(schema.Path)
if !C.Path.IsSafePath(path) { if !C.Path.IsSafePath(path) {
return nil, fmt.Errorf("%w: %s", errSubPath, path) return nil, C.Path.ErrNotSafePath(path)
} }
} }
vehicle = resource.NewHTTPVehicle(schema.URL, path, schema.Proxy, nil, resource.DefaultHttpTimeout, schema.SizeLimit) vehicle = resource.NewHTTPVehicle(schema.URL, path, schema.Proxy, nil, resource.DefaultHttpTimeout, schema.SizeLimit)