VYPR
Medium severityOSV Advisory· Published Nov 21, 2024· Updated Apr 15, 2026

CVE-2024-52309

CVE-2024-52309

Description

SFTPGo is a full-featured and highly configurable SFTP, HTTP/S, FTP/S and WebDAV server - S3, Google Cloud Storage, Azure Blob. One powerful feature of SFTPGo is the ability to have the EventManager execute scripts or run applications in response to certain events. This feature is very common in all software similar to SFTPGo and is generally unrestricted. However, any SFTPGo administrator with permission to run a script has access to the underlying OS/container with the same permissions as the user running SFTPGo. This is unexpected for some SFTPGo administrators who think that there is a clear distinction between accessing the system shell and accessing the SFTPGo WebAdmin UI. To avoid this confusion, running system commands is disabled by default in 2.6.3, and an allow list has been added so that system administrators configuring SFTPGo must explicitly define which commands are allowed to be configured from the WebAdmin UI.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/drakkan/sftpgo/v2Go
>= 2.4.0, < 2.6.32.6.3
sftpgoGo
>= 2.4.0, < 2.6.32.6.3

Affected products

1

Patches

2
b524da11e946

EventManager: disable commands by default

https://github.com/drakkan/sftpgoNicola MurinoNov 10, 2024via ghsa
4 files changed · +47 17
  • internal/common/protocol_test.go+21 4 modified
    @@ -3928,6 +3928,11 @@ func TestEventRule(t *testing.T) {
     	err = os.WriteFile(uploadScriptPath, getUploadScriptContent(movedPath, "", 0), 0755)
     	assert.NoError(t, err)
     
    +	dataprovider.EnabledActionCommands = []string{uploadScriptPath}
    +	defer func() {
    +		dataprovider.EnabledActionCommands = nil
    +	}()
    +
     	action1.Type = dataprovider.ActionTypeCommand
     	action1.Options = dataprovider.BaseEventActionOptions{
     		CmdConfig: dataprovider.EventActionCommandConfig{
    @@ -4265,6 +4270,10 @@ func TestEventRuleDisabledCommand(t *testing.T) {
     			},
     		},
     	}
    +	_, _, err = httpdtest.AddEventAction(a1, http.StatusBadRequest)
    +	assert.NoError(t, err)
    +	// Enable the command to allow saving
    +	dataprovider.EnabledActionCommands = []string{a1.Options.CmdConfig.Cmd}
     	action1, _, err := httpdtest.AddEventAction(a1, http.StatusCreated)
     	assert.NoError(t, err)
     	action2, _, err := httpdtest.AddEventAction(a2, http.StatusCreated)
    @@ -4312,8 +4321,8 @@ func TestEventRuleDisabledCommand(t *testing.T) {
     	}
     	rule, _, err := httpdtest.AddEventRule(r, http.StatusCreated)
     	assert.NoError(t, err)
    -	// restrit command execution
    -	dataprovider.EnabledActionCommands = []string{"/bin/ls"}
    +	// restrict command execution
    +	dataprovider.EnabledActionCommands = nil
     
     	lastReceivedEmail.reset()
     	// create a folder to trigger the rule
    @@ -4335,8 +4344,6 @@ func TestEventRuleDisabledCommand(t *testing.T) {
     	assert.Contains(t, email.Data, fmt.Sprintf("Object name: %s object type: folder", folder.Name))
     	lastReceivedEmail.reset()
     
    -	dataprovider.EnabledActionCommands = nil
    -
     	_, err = httpdtest.RemoveFolder(folder, http.StatusOK)
     	assert.NoError(t, err)
     
    @@ -4368,6 +4375,11 @@ func TestEventRuleProviderEvents(t *testing.T) {
     	err = os.WriteFile(saveObjectScriptPath, getSaveProviderObjectScriptContent(outPath, 0), 0755)
     	assert.NoError(t, err)
     
    +	dataprovider.EnabledActionCommands = []string{saveObjectScriptPath}
    +	defer func() {
    +		dataprovider.EnabledActionCommands = nil
    +	}()
    +
     	a1 := dataprovider.BaseEventAction{
     		Name: "a1",
     		Type: dataprovider.ActionTypeCommand,
    @@ -5231,6 +5243,11 @@ func TestEventActionCommandEnvVars(t *testing.T) {
     	envName := "MY_ENV"
     	uploadScriptPath := filepath.Join(os.TempDir(), "upload.sh")
     
    +	dataprovider.EnabledActionCommands = []string{uploadScriptPath}
    +	defer func() {
    +		dataprovider.EnabledActionCommands = nil
    +	}()
    +
     	err := os.WriteFile(uploadScriptPath, getUploadScriptEnvContent(envName), 0755)
     	assert.NoError(t, err)
     	a1 := dataprovider.BaseEventAction{
    
  • internal/dataprovider/eventrule.go+1 4 modified
    @@ -59,7 +59,7 @@ var (
     		ActionTypeDataRetentionCheck, ActionTypePasswordExpirationCheck, ActionTypeUserExpirationCheck,
     		ActionTypeUserInactivityCheck, ActionTypeIDPAccountCheck, ActionTypeRotateLogs}
     	// EnabledActionCommands defines the system commands that can be executed via EventManager,
    -	// an empty list means that any command is allowed to be executed.
    +	// an empty list means that no command is allowed to be executed.
     	EnabledActionCommands []string
     )
     
    @@ -455,9 +455,6 @@ func (c *EventActionHTTPConfig) GetHTTPClient() *http.Client {
     
     // IsActionCommandAllowed returns true if the specified command is allowed
     func IsActionCommandAllowed(cmd string) bool {
    -	if len(EnabledActionCommands) == 0 {
    -		return true
    -	}
     	return slices.Contains(EnabledActionCommands, cmd)
     }
     
    
  • internal/httpd/httpd_test.go+19 0 modified
    @@ -1840,6 +1840,10 @@ func TestBasicActionRulesHandling(t *testing.T) {
     			},
     		},
     	}
    +	dataprovider.EnabledActionCommands = []string{a.Options.CmdConfig.Cmd}
    +	defer func() {
    +		dataprovider.EnabledActionCommands = nil
    +	}()
     	_, _, err = httpdtest.UpdateEventAction(a, http.StatusOK)
     	assert.NoError(t, err)
     	// invalid type
    @@ -2374,13 +2378,24 @@ func TestEventActionValidation(t *testing.T) {
     	assert.NoError(t, err)
     	assert.Contains(t, string(resp), "command is required")
     	action.Options.CmdConfig.Cmd = "relative"
    +	dataprovider.EnabledActionCommands = []string{action.Options.CmdConfig.Cmd}
    +	defer func() {
    +		dataprovider.EnabledActionCommands = nil
    +	}()
    +
     	_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)
     	assert.NoError(t, err)
     	assert.Contains(t, string(resp), "invalid command, it must be an absolute path")
     	action.Options.CmdConfig.Cmd = filepath.Join(os.TempDir(), "cmd")
     	_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)
     	assert.NoError(t, err)
    +	assert.Contains(t, string(resp), "is not allowed")
    +
    +	dataprovider.EnabledActionCommands = []string{action.Options.CmdConfig.Cmd}
    +	_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)
    +	assert.NoError(t, err)
     	assert.Contains(t, string(resp), "invalid command action timeout")
    +
     	action.Options.CmdConfig.Timeout = 30
     	action.Options.CmdConfig.EnvVars = []dataprovider.KeyValue{
     		{
    @@ -24027,6 +24042,10 @@ func TestWebEventAction(t *testing.T) {
     			},
     		},
     	}
    +	dataprovider.EnabledActionCommands = []string{action.Options.CmdConfig.Cmd}
    +	defer func() {
    +		dataprovider.EnabledActionCommands = nil
    +	}()
     	form.Set("type", fmt.Sprintf("%d", action.Type))
     	req, err = http.NewRequest(http.MethodPost, path.Join(webAdminEventActionPath, action.Name),
     		bytes.NewBuffer([]byte(form.Encode())))
    
  • templates/webadmin/eventaction.html+6 9 modified
    @@ -44,6 +44,11 @@ <h3 data-i18n="{{.Title}}" class="card-title section-title"></h3>
                     <div class="col-md-9">
                         <select id="idType" name="type" class="form-select" data-control="i18n-select2" data-hide-search="true">
                             {{- range .ActionTypes}}
    +                        {{- if eq .Value 2}}
    +                            {{- if not $.EnabledCommands}}
    +                                {{- continue}}
    +                            {{- end}}
    +                        {{- end}}
                             <option value="{{.Value}}" {{if eq $.Action.Type .Value }}selected{{end}} data-i18n="{{.Name}}"></option>
                             {{- end}}
                         </select>
    @@ -400,21 +405,13 @@ <h3 data-i18n="actions.multipart_body" class="card-title section-title-inner">Mu
                 <div class="form-group row action-type action-cmd mt-10">
                     <label for="idCmdPath" data-i18n="actions.types.command" class="col-md-3 col-form-label">Command</label>
                     <div class="col-md-9">
    -                    <select id="idCmdPath" name="cmd_path" class="form-select" data-control="i18n-select2" data-hide-search="true">
    +                    <select id="idCmdPath" name="cmd_path" class="form-select" data-control="i18n-select2" data-hide-search="false">
                             {{- range .EnabledCommands}}
                             <option value="{{.}}" {{if eq $.Action.Options.CmdConfig.Cmd . }}selected{{end}}>{{.}}</option>
                             {{- end}}
                         </select>
                     </div>
                 </div>
    -            {{- else}}
    -            <div class="form-group row action-type action-cmd mt-10">
    -                <label for="idCmdPath" data-i18n="actions.types.command" class="col-md-3 col-form-label">Command</label>
    -                <div class="col-md-9">
    -                    <input id="idCmdPath" type="text" class="form-control" name="cmd_path" value="{{.Action.Options.CmdConfig.Cmd}}" aria-describedby="idCmdPathHelp" />
    -                    <div id="idCmdPathHelp" class="form-text" data-i18n="actions.command_help"></div>
    -                </div>
    -            </div>
                 {{- end}}
     
                 <div class="form-group row action-type action-cmd mt-10">
    
88b1850b5806

EventManager: allow to define the allowed system commands

https://github.com/drakkan/sftpgoNicola MurinoNov 1, 2024via ghsa
10 files changed · +259 17
  • internal/common/common.go+24 1 modified
    @@ -239,6 +239,9 @@ func Initialize(c Configuration, isShared int) error {
     	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)
    @@ -247,6 +250,7 @@ func Initialize(c Configuration, isShared int) error {
     	vfs.SetResumeMaxSize(c.ResumeMaxSize)
     	vfs.SetUploadMode(c.UploadMode)
     	dataprovider.SetAllowSelfConnections(c.AllowSelfConnections)
    +	dataprovider.EnabledActionCommands = c.EventManager.EnabledCommands
     	transfersChecker = getTransfersChecker(isShared)
     	return nil
     }
    @@ -512,6 +516,23 @@ type ConnectionTransfer struct {
     	DLSize        int64  `json:"-"`
     }
     
    +// EventManagerConfig defines the configuration for the EventManager
    +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"`
    +}
    +
    +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
    +}
    +
     // MetadataConfig defines how to handle metadata for cloud storage backends
     type MetadataConfig struct {
     	// If not zero the metadata will be read before downloads and will be
    @@ -621,7 +642,9 @@ type Configuration struct {
     	// server's local time, otherwise UTC will be used.
     	TZ string `json:"tz" mapstructure:"tz"`
     	// Metadata configuration
    -	Metadata              MetadataConfig `json:"metadata" mapstructure:"metadata"`
    +	Metadata MetadataConfig `json:"metadata" mapstructure:"metadata"`
    +	// EventManager configuration
    +	EventManager          EventManagerConfig `json:"event_manager" mapstructure:"event_manager"`
     	idleTimeoutAsDuration time.Duration
     	idleLoginTimeout      time.Duration
     	defender              Defender
    
  • internal/common/common_test.go+27 0 modified
    @@ -217,6 +217,33 @@ func TestConnections(t *testing.T) {
     	Connections.RUnlock()
     }
     
    +func TestEventManagerCommandsInitialization(t *testing.T) {
    +	configCopy := Config
    +
    +	c := Configuration{
    +		EventManager: EventManagerConfig{
    +			EnabledCommands: []string{"ls"}, // not an absolute path
    +		},
    +	}
    +	err := Initialize(c, 0)
    +	assert.ErrorContains(t, err, "invalid command")
    +
    +	var commands []string
    +	if runtime.GOOS == osWindows {
    +		commands = []string{"C:\\command"}
    +	} else {
    +		commands = []string{"/bin/ls"}
    +	}
    +
    +	c.EventManager.EnabledCommands = commands
    +	err = Initialize(c, 0)
    +	assert.NoError(t, err)
    +	assert.Equal(t, commands, dataprovider.EnabledActionCommands)
    +
    +	dataprovider.EnabledActionCommands = configCopy.EventManager.EnabledCommands
    +	Config = configCopy
    +}
    +
     func TestInitializationProxyErrors(t *testing.T) {
     	configCopy := Config
     
    
  • internal/common/eventmanager.go+3 0 modified
    @@ -1484,6 +1484,9 @@ func executeHTTPRuleAction(c dataprovider.EventActionHTTPConfig, params *EventPa
     }
     
     func executeCommandRuleAction(c dataprovider.EventActionCommandConfig, params *EventParams) error {
    +	if !dataprovider.IsActionCommandAllowed(c.Cmd) {
    +		return fmt.Errorf("command %q is not allowed", c.Cmd)
    +	}
     	addObjectData := false
     	if params.Object != nil {
     		for _, k := range c.EnvVars {
    
  • internal/common/protocol_test.go+142 0 modified
    @@ -4208,6 +4208,148 @@ func TestEventRuleStatues(t *testing.T) {
     	require.NoError(t, err)
     }
     
    +func TestEventRuleDisabledCommand(t *testing.T) {
    +	if runtime.GOOS == osWindows {
    +		t.Skip("this test is not available on Windows")
    +	}
    +	smtpCfg := smtp.Config{
    +		Host:          "127.0.0.1",
    +		Port:          2525,
    +		From:          "notification@example.com",
    +		TemplatesPath: "templates",
    +	}
    +	err := smtpCfg.Initialize(configDir, true)
    +	require.NoError(t, err)
    +
    +	saveObjectScriptPath := filepath.Join(os.TempDir(), "provider.sh")
    +	outPath := filepath.Join(os.TempDir(), "provider_out.json")
    +	err = os.WriteFile(saveObjectScriptPath, getSaveProviderObjectScriptContent(outPath, 0), 0755)
    +	assert.NoError(t, err)
    +
    +	a1 := dataprovider.BaseEventAction{
    +		Name: "a1",
    +		Type: dataprovider.ActionTypeCommand,
    +		Options: dataprovider.BaseEventActionOptions{
    +			CmdConfig: dataprovider.EventActionCommandConfig{
    +				Cmd:     saveObjectScriptPath,
    +				Timeout: 10,
    +				EnvVars: []dataprovider.KeyValue{
    +					{
    +						Key:   "SFTPGO_OBJECT_DATA",
    +						Value: "{{ObjectData}}",
    +					},
    +				},
    +			},
    +		},
    +	}
    +	a2 := dataprovider.BaseEventAction{
    +		Name: "a2",
    +		Type: dataprovider.ActionTypeEmail,
    +		Options: dataprovider.BaseEventActionOptions{
    +			EmailConfig: dataprovider.EventActionEmailConfig{
    +				Recipients: []string{"test3@example.com"},
    +				Subject:    `New "{{Event}}" from "{{Name}}"`,
    +				Body:       "Object name: {{ObjectName}} object type: {{ObjectType}} Data: {{ObjectData}}",
    +			},
    +		},
    +	}
    +
    +	a3 := dataprovider.BaseEventAction{
    +		Name: "a3",
    +		Type: dataprovider.ActionTypeEmail,
    +		Options: dataprovider.BaseEventActionOptions{
    +			EmailConfig: dataprovider.EventActionEmailConfig{
    +				Recipients: []string{"failure@example.com"},
    +				Subject:    `Failed "{{Event}}" from "{{Name}}"`,
    +				Body:       "Object name: {{ObjectName}} object type: {{ObjectType}}, IP: {{IP}}",
    +			},
    +		},
    +	}
    +	action1, _, err := httpdtest.AddEventAction(a1, http.StatusCreated)
    +	assert.NoError(t, err)
    +	action2, _, err := httpdtest.AddEventAction(a2, http.StatusCreated)
    +	assert.NoError(t, err)
    +	action3, _, err := httpdtest.AddEventAction(a3, http.StatusCreated)
    +	assert.NoError(t, err)
    +
    +	r := dataprovider.EventRule{
    +		Name:    "rule",
    +		Status:  1,
    +		Trigger: dataprovider.EventTriggerProviderEvent,
    +		Conditions: dataprovider.EventConditions{
    +			ProviderEvents: []string{"add"},
    +			Options: dataprovider.ConditionOptions{
    +				ProviderObjects: []string{"folder"},
    +			},
    +		},
    +		Actions: []dataprovider.EventAction{
    +			{
    +				BaseEventAction: dataprovider.BaseEventAction{
    +					Name: action1.Name,
    +				},
    +				Order: 1,
    +				Options: dataprovider.EventActionOptions{
    +					StopOnFailure: true,
    +				},
    +			},
    +			{
    +				BaseEventAction: dataprovider.BaseEventAction{
    +					Name: action2.Name,
    +				},
    +				Order: 2,
    +			},
    +			{
    +				BaseEventAction: dataprovider.BaseEventAction{
    +					Name: action3.Name,
    +				},
    +				Order: 3,
    +				Options: dataprovider.EventActionOptions{
    +					IsFailureAction: true,
    +					StopOnFailure:   true,
    +				},
    +			},
    +		},
    +	}
    +	rule, _, err := httpdtest.AddEventRule(r, http.StatusCreated)
    +	assert.NoError(t, err)
    +	// restrit command execution
    +	dataprovider.EnabledActionCommands = []string{"/bin/ls"}
    +
    +	lastReceivedEmail.reset()
    +	// create a folder to trigger the rule
    +	folder := vfs.BaseVirtualFolder{
    +		Name:       "ftest failed command",
    +		MappedPath: filepath.Join(os.TempDir(), "p"),
    +	}
    +	folder, _, err = httpdtest.AddFolder(folder, http.StatusCreated)
    +	assert.NoError(t, err)
    +
    +	assert.NoFileExists(t, outPath)
    +	assert.Eventually(t, func() bool {
    +		return lastReceivedEmail.get().From != ""
    +	}, 3000*time.Millisecond, 100*time.Millisecond)
    +	email := lastReceivedEmail.get()
    +	assert.Len(t, email.To, 1)
    +	assert.True(t, slices.Contains(email.To, "failure@example.com"))
    +	assert.Contains(t, email.Data, `Subject: Failed "add" from "admin"`)
    +	assert.Contains(t, email.Data, fmt.Sprintf("Object name: %s object type: folder", folder.Name))
    +	lastReceivedEmail.reset()
    +
    +	dataprovider.EnabledActionCommands = nil
    +
    +	_, err = httpdtest.RemoveFolder(folder, http.StatusOK)
    +	assert.NoError(t, err)
    +
    +	_, err = httpdtest.RemoveEventRule(rule, http.StatusOK)
    +	assert.NoError(t, err)
    +	_, err = httpdtest.RemoveEventAction(action1, http.StatusOK)
    +	assert.NoError(t, err)
    +	_, err = httpdtest.RemoveEventAction(action2, http.StatusOK)
    +	assert.NoError(t, err)
    +	_, err = httpdtest.RemoveEventAction(action3, http.StatusOK)
    +	assert.NoError(t, err)
    +}
    +
     func TestEventRuleProviderEvents(t *testing.T) {
     	if runtime.GOOS == osWindows {
     		t.Skip("this test is not available on Windows")
    
  • internal/config/config.go+4 0 modified
    @@ -242,6 +242,9 @@ func Init() {
     			Metadata: common.MetadataConfig{
     				Read: 0,
     			},
    +			EventManager: common.EventManagerConfig{
    +				EnabledCommands: []string{},
    +			},
     		},
     		ACME: acme.Configuration{
     			Email:      "",
    @@ -2032,6 +2035,7 @@ func setViperDefaults() {
     	viper.SetDefault("common.server_version", globalConf.Common.ServerVersion)
     	viper.SetDefault("common.tz", globalConf.Common.TZ)
     	viper.SetDefault("common.metadata.read", globalConf.Common.Metadata.Read)
    +	viper.SetDefault("common.event_manager.enabled_commands", globalConf.Common.EventManager.EnabledCommands)
     	viper.SetDefault("acme.email", globalConf.ACME.Email)
     	viper.SetDefault("acme.key_type", globalConf.ACME.KeyType)
     	viper.SetDefault("acme.certs_path", globalConf.ACME.CertsPath)
    
  • internal/dataprovider/eventrule.go+14 0 modified
    @@ -58,6 +58,9 @@ var (
     		ActionTypeBackup, ActionTypeUserQuotaReset, ActionTypeFolderQuotaReset, ActionTypeTransferQuotaReset,
     		ActionTypeDataRetentionCheck, ActionTypePasswordExpirationCheck, ActionTypeUserExpirationCheck,
     		ActionTypeUserInactivityCheck, ActionTypeIDPAccountCheck, ActionTypeRotateLogs}
    +	// EnabledActionCommands defines the system commands that can be executed via EventManager,
    +	// an empty list means that any command is allowed to be executed.
    +	EnabledActionCommands []string
     )
     
     func isActionTypeValid(action int) bool {
    @@ -450,6 +453,14 @@ func (c *EventActionHTTPConfig) GetHTTPClient() *http.Client {
     	return client
     }
     
    +// IsActionCommandAllowed returns true if the specified command is allowed
    +func IsActionCommandAllowed(cmd string) bool {
    +	if len(EnabledActionCommands) == 0 {
    +		return true
    +	}
    +	return slices.Contains(EnabledActionCommands, cmd)
    +}
    +
     // EventActionCommandConfig defines the configuration for a command event target
     type EventActionCommandConfig struct {
     	Cmd     string     `json:"cmd,omitempty"`
    @@ -462,6 +473,9 @@ func (c *EventActionCommandConfig) validate() error {
     	if c.Cmd == "" {
     		return util.NewI18nError(util.NewValidationError("command is required"), util.I18nErrorCommandRequired)
     	}
    +	if !IsActionCommandAllowed(c.Cmd) {
    +		return util.NewValidationError(fmt.Sprintf("command %q is not allowed", c.Cmd))
    +	}
     	if !filepath.IsAbs(c.Cmd) {
     		return util.NewI18nError(
     			util.NewValidationError("invalid command, it must be an absolute path"),
    
  • internal/httpd/httpd_test.go+11 0 modified
    @@ -2395,6 +2395,17 @@ func TestEventActionValidation(t *testing.T) {
     	_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)
     	assert.NoError(t, err)
     	assert.Contains(t, string(resp), "invalid command args")
    +	action.Options.CmdConfig.Args = nil
    +	// restrict commands
    +	if runtime.GOOS == osWindows {
    +		dataprovider.EnabledActionCommands = []string{"C:\\cmd.exe"}
    +	} else {
    +		dataprovider.EnabledActionCommands = []string{"/bin/sh"}
    +	}
    +	_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)
    +	assert.NoError(t, err)
    +	assert.Contains(t, string(resp), "is not allowed")
    +	dataprovider.EnabledActionCommands = nil
     
     	action.Type = dataprovider.ActionTypeEmail
     	_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)
    
  • internal/httpd/webadmin.go+17 15 modified
    @@ -297,13 +297,14 @@ type rolePage struct {
     
     type eventActionPage struct {
     	basePage
    -	Action         dataprovider.BaseEventAction
    -	ActionTypes    []dataprovider.EnumMapping
    -	FsActions      []dataprovider.EnumMapping
    -	HTTPMethods    []string
    -	RedactedSecret string
    -	Error          *util.I18nError
    -	Mode           genericPageMode
    +	Action          dataprovider.BaseEventAction
    +	ActionTypes     []dataprovider.EnumMapping
    +	FsActions       []dataprovider.EnumMapping
    +	HTTPMethods     []string
    +	EnabledCommands []string
    +	RedactedSecret  string
    +	Error           *util.I18nError
    +	Mode            genericPageMode
     }
     
     type eventRulePage struct {
    @@ -1088,14 +1089,15 @@ func (s *httpdServer) renderEventActionPage(w http.ResponseWriter, r *http.Reque
     	}
     
     	data := eventActionPage{
    -		basePage:       s.getBasePageData(title, currentURL, w, r),
    -		Action:         action,
    -		ActionTypes:    dataprovider.EventActionTypes,
    -		FsActions:      dataprovider.FsActionTypes,
    -		HTTPMethods:    dataprovider.SupportedHTTPActionMethods,
    -		RedactedSecret: redactedSecret,
    -		Error:          getI18nError(err),
    -		Mode:           mode,
    +		basePage:        s.getBasePageData(title, currentURL, w, r),
    +		Action:          action,
    +		ActionTypes:     dataprovider.EventActionTypes,
    +		FsActions:       dataprovider.FsActionTypes,
    +		HTTPMethods:     dataprovider.SupportedHTTPActionMethods,
    +		EnabledCommands: dataprovider.EnabledActionCommands,
    +		RedactedSecret:  redactedSecret,
    +		Error:           getI18nError(err),
    +		Mode:            mode,
     	}
     	renderAdminTemplate(w, templateEventAction, data)
     }
    
  • sftpgo.json+4 1 modified
    @@ -62,7 +62,10 @@
             "entries_soft_limit": 100,
             "entries_hard_limit": 150
           }
    -    ]
    +    ],
    +    "event_manager": {
    +      "enabled_commands": []
    +    }
       },
       "acme": {
         "domains": [],
    
  • templates/webadmin/eventaction.html+13 0 modified
    @@ -396,13 +396,26 @@ <h3 data-i18n="actions.multipart_body" class="card-title section-title-inner">Mu
                     </div>
                 </div>
     
    +            {{ if .EnabledCommands}}
    +            <div class="form-group row action-type action-cmd mt-10">
    +                <label for="idCmdPath" data-i18n="actions.types.command" class="col-md-3 col-form-label">Command</label>
    +                <div class="col-md-9">
    +                    <select id="idCmdPath" name="cmd_path" class="form-select" data-control="i18n-select2" data-hide-search="true">
    +                        {{- range .EnabledCommands}}
    +                        <option value="{{.}}" {{if eq $.Action.Options.CmdConfig.Cmd . }}selected{{end}}>{{.}}</option>
    +                        {{- end}}
    +                    </select>
    +                </div>
    +            </div>
    +            {{- else}}
                 <div class="form-group row action-type action-cmd mt-10">
                     <label for="idCmdPath" data-i18n="actions.types.command" class="col-md-3 col-form-label">Command</label>
                     <div class="col-md-9">
                         <input id="idCmdPath" type="text" class="form-control" name="cmd_path" value="{{.Action.Options.CmdConfig.Cmd}}" aria-describedby="idCmdPathHelp" />
                         <div id="idCmdPathHelp" class="form-text" data-i18n="actions.command_help"></div>
                     </div>
                 </div>
    +            {{- end}}
     
                 <div class="form-group row action-type action-cmd mt-10">
                     <label for="idCommandArgs" data-i18n="actions.command_args" class="col-md-3 col-form-label">Arguments</label>
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

6

News mentions

0

No linked articles in our index yet.