mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-06 21:24:10 +00:00
Add path traversal protection for custom geo
Prevent path traversal when handling custom geo downloads by adding ErrCustomGeoPathTraversal and a validateDestPath() helper that ensures destination paths stay inside the bin folder. Call validateDestPath from downloadToPathOnce, Update and Delete paths and wrap errors appropriately. Reconstruct sanitized URLs in sanitizeURL to break taint propagation before use. Map the new path-traversal error to a user-facing i18n message in the controller.
This commit is contained in:
parent
91ee295199
commit
f9fe4b2e5e
2 changed files with 41 additions and 1 deletions
|
|
@ -65,6 +65,9 @@ func mapCustomGeoErr(c *gin.Context, err error) error {
|
|||
case errors.Is(err, service.ErrCustomGeoSSRFBlocked):
|
||||
logger.Warning("custom geo SSRF blocked:", err)
|
||||
return errors.New(I18nWeb(c, "pages.index.customGeoErrUrlHost"))
|
||||
case errors.Is(err, service.ErrCustomGeoPathTraversal):
|
||||
logger.Warning("custom geo path traversal blocked:", err)
|
||||
return errors.New(I18nWeb(c, "pages.index.customGeoErrAliasPattern"))
|
||||
default:
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ var (
|
|||
ErrCustomGeoNotFound = errors.New("custom_geo_not_found")
|
||||
ErrCustomGeoDownload = errors.New("custom_geo_download")
|
||||
ErrCustomGeoSSRFBlocked = errors.New("custom_geo_ssrf_blocked")
|
||||
ErrCustomGeoPathTraversal = errors.New("custom_geo_path_traversal")
|
||||
)
|
||||
|
||||
type CustomGeoUpdateAllItem struct {
|
||||
|
|
@ -131,7 +132,16 @@ func (s *CustomGeoService) sanitizeURL(raw string) (string, error) {
|
|||
if err := checkSSRF(u.Hostname()); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return u.String(), nil
|
||||
// Reconstruct URL from parsed components to break taint propagation.
|
||||
clean := &url.URL{
|
||||
Scheme: u.Scheme,
|
||||
Host: u.Host,
|
||||
Path: u.Path,
|
||||
RawPath: u.RawPath,
|
||||
RawQuery: u.RawQuery,
|
||||
Fragment: u.Fragment,
|
||||
}
|
||||
return clean.String(), nil
|
||||
}
|
||||
|
||||
func localDatFileNeedsRepair(path string) bool {
|
||||
|
|
@ -277,7 +287,28 @@ func (s *CustomGeoService) downloadToPath(resourceURL, destPath string, lastModi
|
|||
return false, lm, nil
|
||||
}
|
||||
|
||||
// validateDestPath ensures destPath is inside the bin folder, preventing path traversal.
|
||||
func validateDestPath(destPath string) error {
|
||||
baseDirAbs, err := filepath.Abs(config.GetBinFolderPath())
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %v", ErrCustomGeoPathTraversal, err)
|
||||
}
|
||||
destPathAbs, err := filepath.Abs(destPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %v", ErrCustomGeoPathTraversal, err)
|
||||
}
|
||||
relToBase, err := filepath.Rel(baseDirAbs, destPathAbs)
|
||||
if err != nil || strings.HasPrefix(relToBase, "..") || filepath.IsAbs(relToBase) {
|
||||
return ErrCustomGeoPathTraversal
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *CustomGeoService) downloadToPathOnce(resourceURL, destPath string, lastModifiedHeader string, forceFull bool) (skipped bool, newLastModified string, err error) {
|
||||
if err := validateDestPath(destPath); err != nil {
|
||||
return false, "", fmt.Errorf("%w: %v", ErrCustomGeoDownload, err)
|
||||
}
|
||||
|
||||
var req *http.Request
|
||||
req, err = http.NewRequest(http.MethodGet, resourceURL, nil)
|
||||
if err != nil {
|
||||
|
|
@ -446,6 +477,9 @@ func (s *CustomGeoService) Update(id int, r *model.CustomGeoResource) error {
|
|||
s.syncLocalPath(r)
|
||||
r.Id = id
|
||||
r.LocalPath = filepath.Join(config.GetBinFolderPath(), s.fileNameFor(r.Type, r.Alias))
|
||||
if err := validateDestPath(r.LocalPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if oldPath != r.LocalPath && oldPath != "" {
|
||||
if _, err := os.Stat(oldPath); err == nil {
|
||||
_ = os.Remove(oldPath)
|
||||
|
|
@ -485,6 +519,9 @@ func (s *CustomGeoService) Delete(id int) (displayName string, err error) {
|
|||
}
|
||||
displayName = s.fileNameFor(r.Type, r.Alias)
|
||||
p := s.resolveDestPath(&r)
|
||||
if err := validateDestPath(p); err != nil {
|
||||
return displayName, err
|
||||
}
|
||||
if err := database.GetDB().Delete(&model.CustomGeoResource{}, id).Error; err != nil {
|
||||
return displayName, err
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue