CVE-2024-52309 (Go)

SFTPGo's EventManager allows administrators to configure command actions that run operating system commands when events occur. In the vulnerable behavior, an authenticated administrator who can access the WebAdmin or management API can configure arbitrary system commands as EventManager actions, even when those commands have not been explicitly approved by the system operator. This gives that administrator a path to execute commands on the host or container with the privileges of the SFTPGo process, which may be broader than the access they expect to have through the SFTPGo administration interface.

Weakness:
CWE-20: name: Improper Input Validation
description: The product receives input or data, but it does
        not validate or incorrectly validates that the input has the
        properties that are required to process the data safely and
        correctly.

Repository: https://github.com/drakkan/sftpgo

Vulnerable code:
file_path: internal/common/common.go
snippet: func Initialize(c Configuration, isShared int) error {
	isShuttingDown.Store(false)
	util.SetUmask(c.Umask)
	version.SetConfig(c.ServerVersion)
	dataprovider.SetTZ(c.TZ)
	Config = c
	Config.Actions.ExecuteOn = util.RemoveDuplicates(Config.Actions.ExecuteOn, true)
	Config.Actions.ExecuteSync = util.RemoveDuplicates(Config.Actions.ExecuteSync, true)
	Config.ProxyAllowed = util.RemoveDuplicates(Config.ProxyAllowed, true)
	Config.idleLoginTimeout = 2 * time.Minute
	Config.idleTimeoutAsDuration = time.Duration(Config.IdleTimeout) * time.Minute
	startPeriodicChecks(periodicTimeoutCheckInterval, isShared)
	Config.defender = nil
	Config.allowList = nil
	Config.rateLimitersList = nil
	rateLimiters = make(map[string][]*rateLimiter)
	for _, rlCfg := range c.RateLimitersConfig {
		if rlCfg.isEnabled() {
			if err := rlCfg.validate(); err != nil {
				return fmt.Errorf("rate limiters initialization error: %w", err)
			}
			rateLimiter := rlCfg.getLimiter()
			for _, protocol := range rlCfg.Protocols {
				rateLimiters[protocol] = append(rateLimiters[protocol], rateLimiter)
			}
		}
	}
	if len(rateLimiters) > 0 {
		rateLimitersList, err := dataprovider.NewIPList(dataprovider.IPListTypeRateLimiterSafeList)
		if err != nil {
			return fmt.Errorf("unable to initialize ratelimiters list: %w", err)
		}
		Config.rateLimitersList = rateLimitersList
	}
	if c.DefenderConfig.Enabled {
		if !slices.Contains(supportedDefenderDrivers, c.DefenderConfig.Driver) {
			return fmt.Errorf("unsupported defender driver %q", c.DefenderConfig.Driver)
		}
		var defender Defender
		var err error
		switch c.DefenderConfig.Driver {
		case DefenderDriverProvider:
			defender, err = newDBDefender(&c.DefenderConfig)
		default:
			defender, err = newInMemoryDefender(&c.DefenderConfig)
		}
		if err != nil {
			return fmt.Errorf("defender initialization error: %v", err)
		}
		logger.Info(logSender, "", "defender initialized with config %+v", c.DefenderConfig)
		Config.defender = defender
	}
	if c.AllowListStatus > 0 {
		allowList, err := dataprovider.NewIPList(dataprovider.IPListTypeAllowList)
		if err != nil {
			return fmt.Errorf("unable to initialize the allow list: %w", err)
		}
		logger.Info(logSender, "", "allow list initialized")
		Config.allowList = allowList
	}
	if err := c.initializeProxyProtocol(); err != nil {
		return err
	}
	vfs.SetTempPath(c.TempPath)
	dataprovider.SetTempPath(c.TempPath)
	vfs.SetAllowSelfConnections(c.AllowSelfConnections)
	vfs.SetRenameMode(c.RenameMode)
	vfs.SetReadMetadataMode(c.Metadata.Read)
	vfs.SetResumeMaxSize(c.ResumeMaxSize)
	vfs.SetUploadMode(c.UploadMode)
	dataprovider.SetAllowSelfConnections(c.AllowSelfConnections)
	transfersChecker = getTransfersChecker(isShared)
	return nil
}

Fixed code:
file_path: internal/common/common.go
snippet: func Initialize(c Configuration, isShared int) error {
	isShuttingDown.Store(false)
	util.SetUmask(c.Umask)
	version.SetConfig(c.ServerVersion)
	dataprovider.SetTZ(c.TZ)
	Config = c
	Config.Actions.ExecuteOn = util.RemoveDuplicates(Config.Actions.ExecuteOn, true)
	Config.Actions.ExecuteSync = util.RemoveDuplicates(Config.Actions.ExecuteSync, true)
	Config.ProxyAllowed = util.RemoveDuplicates(Config.ProxyAllowed, true)
	Config.idleLoginTimeout = 2 * time.Minute
	Config.idleTimeoutAsDuration = time.Duration(Config.IdleTimeout) * time.Minute
	startPeriodicChecks(periodicTimeoutCheckInterval, isShared)
	Config.defender = nil
	Config.allowList = nil
	Config.rateLimitersList = nil
	rateLimiters = make(map[string][]*rateLimiter)
	for _, rlCfg := range c.RateLimitersConfig {
		if rlCfg.isEnabled() {
			if err := rlCfg.validate(); err != nil {
				return fmt.Errorf("rate limiters initialization error: %w", err)
			}
			rateLimiter := rlCfg.getLimiter()
			for _, protocol := range rlCfg.Protocols {
				rateLimiters[protocol] = append(rateLimiters[protocol], rateLimiter)
			}
		}
	}
	if len(rateLimiters) > 0 {
		rateLimitersList, err := dataprovider.NewIPList(dataprovider.IPListTypeRateLimiterSafeList)
		if err != nil {
			return fmt.Errorf("unable to initialize ratelimiters list: %w", err)
		}
		Config.rateLimitersList = rateLimitersList
	}
	if c.DefenderConfig.Enabled {
		if !slices.Contains(supportedDefenderDrivers, c.DefenderConfig.Driver) {
			return fmt.Errorf("unsupported defender driver %q", c.DefenderConfig.Driver)
		}
		var defender Defender
		var err error
		switch c.DefenderConfig.Driver {
		case DefenderDriverProvider:
			defender, err = newDBDefender(&c.DefenderConfig)
		default:
			defender, err = newInMemoryDefender(&c.DefenderConfig)
		}
		if err != nil {
			return fmt.Errorf("defender initialization error: %v", err)
		}
		logger.Info(logSender, "", "defender initialized with config %+v", c.DefenderConfig)
		Config.defender = defender
	}
	if c.AllowListStatus > 0 {
		allowList, err := dataprovider.NewIPList(dataprovider.IPListTypeAllowList)
		if err != nil {
			return fmt.Errorf("unable to initialize the allow list: %w", err)
		}
		logger.Info(logSender, "", "allow list initialized")
		Config.allowList = allowList
	}
	if err := c.initializeProxyProtocol(); err != nil {
		return err
	}
	if err := c.EventManager.validate(); err != nil {
		return err
	}
	vfs.SetTempPath(c.TempPath)
	dataprovider.SetTempPath(c.TempPath)
	vfs.SetAllowSelfConnections(c.AllowSelfConnections)
	vfs.SetRenameMode(c.RenameMode)
	vfs.SetReadMetadataMode(c.Metadata.Read)
	vfs.SetResumeMaxSize(c.ResumeMaxSize)
	vfs.SetUploadMode(c.UploadMode)
	dataprovider.SetAllowSelfConnections(c.AllowSelfConnections)
	dataprovider.EnabledActionCommands = c.EventManager.EnabledCommands
	transfersChecker = getTransfersChecker(isShared)
	return nil
}

file_path: internal/common/common.go
snippet: type EventManagerConfig struct {
	// EnabledCommands defines the system commands that can be executed via EventManager,
	// an empty list means that any command is allowed to be executed.
	// Commands must be set as an absolute path
	EnabledCommands []string `json:"enabled_commands" mapstructure:"enabled_commands"`
}

file_path: internal/common/common.go
snippet: func (c *EventManagerConfig) validate() error {
	for _, c := range c.EnabledCommands {
		if !filepath.IsAbs(c) {
			return fmt.Errorf("invalid command %q: it must be an absolute path", c)
		}
	}
	return nil
}