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:
MHSanaei 2026-04-19 23:30:53 +02:00
parent 91ee295199
commit f9fe4b2e5e
No known key found for this signature in database
GPG key ID: 7E4060F2FBE5AB7A
2 changed files with 41 additions and 1 deletions

View file

@ -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
}

View file

@ -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
}