diff --git a/adapter/outbound/ssh.go b/adapter/outbound/ssh.go index 861999ed6..3b9151474 100644 --- a/adapter/outbound/ssh.go +++ b/adapter/outbound/ssh.go @@ -138,7 +138,7 @@ func NewSsh(option SshOption) (*Ssh, error) { } else { path := C.Path.Resolve(option.PrivateKey) 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) if err != nil { diff --git a/adapter/provider/parser.go b/adapter/provider/parser.go index 2eaa2a3e9..6907fc69b 100644 --- a/adapter/provider/parser.go +++ b/adapter/provider/parser.go @@ -17,7 +17,6 @@ import ( var ( errVehicleType = errors.New("unsupport vehicle type") - errSubPath = errors.New("path is not subpath of home directory") ) type healthCheckSchema struct { @@ -115,7 +114,7 @@ func ParseProxyProvider(name string, mapping map[string]any) (types.ProxyProvide if schema.Path != "" { path = C.Path.Resolve(schema.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) diff --git a/component/ca/config.go b/component/ca/config.go index 4b37f7624..f4d3ae751 100644 --- a/component/ca/config.go +++ b/component/ca/config.go @@ -83,7 +83,7 @@ func GetCertPool(customCA string, customCAString string) (*x509.CertPool, error) if len(customCA) > 0 { path := C.Path.Resolve(customCA) 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) if err != nil { diff --git a/component/ca/keypair.go b/component/ca/keypair.go index 51555f6e3..7fa6c21fe 100644 --- a/component/ca/keypair.go +++ b/component/ca/keypair.go @@ -17,6 +17,7 @@ import ( type Path interface { Resolve(path string) string 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. @@ -42,10 +43,12 @@ func LoadTLSKeyPair(certificate, privateKey string, path Path) (tls.Certificate, certificate = path.Resolve(certificate) privateKey = path.Resolve(privateKey) var loadErr error - if path.IsSafePath(certificate) && path.IsSafePath(privateKey) { - cert, loadErr = tls.LoadX509KeyPair(certificate, privateKey) + if !path.IsSafePath(certificate) { + loadErr = path.ErrNotSafePath(certificate) + } else if !path.IsSafePath(privateKey) { + loadErr = path.ErrNotSafePath(privateKey) } else { - loadErr = fmt.Errorf("path is not subpath of home directory") + cert, loadErr = tls.LoadX509KeyPair(certificate, privateKey) } if loadErr != nil { return tls.Certificate{}, fmt.Errorf("parse certificate failed, maybe format error:%s, or path error: %s", painTextErr.Error(), loadErr.Error()) diff --git a/config/config.go b/config/config.go index f6b193d45..56b2aa7bc 100644 --- a/config/config.go +++ b/config/config.go @@ -755,7 +755,7 @@ func parseGeneral(cfg *RawConfig) (*General, error) { func parseController(cfg *RawConfig) (*Controller, error) { 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{ ExternalController: cfg.ExternalController, diff --git a/constant/path.go b/constant/path.go index 909e18f61..a7f180b5c 100644 --- a/constant/path.go +++ b/constant/path.go @@ -1,6 +1,7 @@ package constant import ( + "fmt" "os" P "path" "path/filepath" @@ -87,10 +88,8 @@ func (p *path) IsSafePath(path string) bool { if p.allowUnsafePath || features.CMFA { return true } - homedir := p.HomeDir() path = p.Resolve(path) - safePaths := append([]string{homedir}, p.safePaths...) // add homedir to safePaths - for _, safePath := range safePaths { + for _, safePath := range p.SafePaths() { if rel, err := filepath.Rel(safePath, path); err == nil { if filepath.IsLocal(rel) { return true @@ -100,6 +99,23 @@ func (p *path) IsSafePath(path string) bool { 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 { hash := utils.MakeHash([]byte(name)) filename := hash.String() diff --git a/hub/route/configs.go b/hub/route/configs.go index c43d3ef97..c387c9498 100644 --- a/hub/route/configs.go +++ b/hub/route/configs.go @@ -371,12 +371,20 @@ func updateConfigs(w http.ResponseWriter, r *http.Request) { return } } else { - if req.Path == "" { + if req.Path == "" { // default path unneeded any safe check req.Path = C.Path.Config() - } else if !filepath.IsAbs(req.Path) { - render.Status(r, http.StatusBadRequest) - render.JSON(w, r, newError("path is not a absolute path")) - return + } else { + if !filepath.IsAbs(req.Path) { + render.Status(r, http.StatusBadRequest) + 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) diff --git a/rules/provider/parse.go b/rules/provider/parse.go index 4683aa386..5fa98dc7c 100644 --- a/rules/provider/parse.go +++ b/rules/provider/parse.go @@ -1,7 +1,6 @@ package provider import ( - "errors" "fmt" "time" @@ -12,10 +11,6 @@ import ( "github.com/metacubex/mihomo/rules/common" ) -var ( - errSubPath = errors.New("path is not subpath of home directory") -) - type ruleProviderSchema struct { Type string `provider:"type"` Behavior string `provider:"behavior"` @@ -53,7 +48,7 @@ func ParseRuleProvider(name string, mapping map[string]any, parse common.ParseRu if schema.Path != "" { path = C.Path.Resolve(schema.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)