From 9a41726bc40d3cdbd88aba6d9b2f85fab2f57d0e Mon Sep 17 00:00:00 2001 From: andres-portainer Date: Wed, 15 Nov 2023 16:54:59 -0300 Subject: [PATCH] fix(stacks): validate the build context EE-6211 --- .../deployments/deployment_compose_config.go | 19 +++++-- .../deployments/deployment_swarm_config.go | 17 +++++-- api/stacks/stackutils/validation.go | 51 +++++++++++++++---- .../internal/composeplugin/composeplugin.go | 8 ++- 4 files changed, 73 insertions(+), 22 deletions(-) diff --git a/api/stacks/deployments/deployment_compose_config.go b/api/stacks/deployments/deployment_compose_config.go index 199928a87..248eda5ab 100644 --- a/api/stacks/deployments/deployment_compose_config.go +++ b/api/stacks/deployments/deployment_compose_config.go @@ -2,13 +2,14 @@ package deployments import ( "fmt" - "log" - "github.com/pkg/errors" portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/dataservices" "github.com/portainer/portainer/api/http/security" "github.com/portainer/portainer/api/stacks/stackutils" + + "github.com/pkg/errors" + "github.com/rs/zerolog/log" ) type ComposeStackDeploymentConfig struct { @@ -55,15 +56,22 @@ func (config *ComposeStackDeploymentConfig) GetUsername() string { if config.user != nil { return config.user.Username } + return "" } func (config *ComposeStackDeploymentConfig) Deploy() error { if config.FileService == nil || config.StackDeployer == nil { - log.Println("[deployment, compose] file service or stack deployer is not initialised") + log.Error().Msg("file service or stack deployer is not initialised") + return errors.New("file service or stack deployer cannot be nil") } + err := stackutils.ValidateStackFiles(config.stack, stackutils.IsValidBuildContext, config.FileService) + if err != nil { + return err + } + isAdminOrEndpointAdmin, err := stackutils.UserIsAdminOrEndpointAdmin(config.user, config.endpoint.ID) if err != nil { return errors.Wrap(err, "failed to validate user admin privileges") @@ -79,11 +87,14 @@ func (config *ComposeStackDeploymentConfig) Deploy() error { !securitySettings.AllowContainerCapabilitiesForRegularUsers) && !isAdminOrEndpointAdmin { - err = stackutils.ValidateStackFiles(config.stack, securitySettings, config.FileService) + validStackFn := stackutils.IsValidStackFileAdapter(securitySettings) + + err = stackutils.ValidateStackFiles(config.stack, validStackFn, config.FileService) if err != nil { return err } } + if stackutils.IsRelativePathStack(config.stack) { return config.StackDeployer.DeployRemoteComposeStack(config.stack, config.endpoint, config.registries, config.forcePullImage, config.ForceCreate) } diff --git a/api/stacks/deployments/deployment_swarm_config.go b/api/stacks/deployments/deployment_swarm_config.go index da0c6b8c1..97ca38441 100644 --- a/api/stacks/deployments/deployment_swarm_config.go +++ b/api/stacks/deployments/deployment_swarm_config.go @@ -2,13 +2,14 @@ package deployments import ( "fmt" - "log" - "github.com/pkg/errors" portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/dataservices" "github.com/portainer/portainer/api/http/security" "github.com/portainer/portainer/api/stacks/stackutils" + + "github.com/pkg/errors" + "github.com/rs/zerolog/log" ) type SwarmStackDeploymentConfig struct { @@ -60,10 +61,16 @@ func (config *SwarmStackDeploymentConfig) GetUsername() string { func (config *SwarmStackDeploymentConfig) Deploy() error { if config.FileService == nil || config.StackDeployer == nil { - log.Println("[deployment, swarm] file service or stack deployer is not initialised") + log.Error().Msg("file service or stack deployer is not initialised") + return errors.New("file service or stack deployer cannot be nil") } + err := stackutils.ValidateStackFiles(config.stack, stackutils.IsValidBuildContext, config.FileService) + if err != nil { + return err + } + isAdminOrEndpointAdmin, err := stackutils.UserIsAdminOrEndpointAdmin(config.user, config.endpoint.ID) if err != nil { return errors.Wrap(err, "failed to validate user admin privileges") @@ -72,7 +79,9 @@ func (config *SwarmStackDeploymentConfig) Deploy() error { settings := &config.endpoint.SecuritySettings if !settings.AllowBindMountsForRegularUsers && !isAdminOrEndpointAdmin { - err = stackutils.ValidateStackFiles(config.stack, settings, config.FileService) + validStackFn := stackutils.IsValidStackFileAdapter(settings) + + err = stackutils.ValidateStackFiles(config.stack, validStackFn, config.FileService) if err != nil { return err } diff --git a/api/stacks/stackutils/validation.go b/api/stacks/stackutils/validation.go index 83850497e..39726f6b5 100644 --- a/api/stacks/stackutils/validation.go +++ b/api/stacks/stackutils/validation.go @@ -1,16 +1,19 @@ package stackutils import ( + "strings" + + portainer "github.com/portainer/portainer/api" + "github.com/docker/cli/cli/compose/loader" "github.com/docker/cli/cli/compose/types" "github.com/pkg/errors" - portainer "github.com/portainer/portainer/api" ) -func IsValidStackFile(stackFileContent []byte, securitySettings *portainer.EndpointSecuritySettings) error { +func loadComposeConfig(stackFileContent []byte) (*types.Config, error) { composeConfigYAML, err := loader.ParseYAML(stackFileContent) if err != nil { - return err + return nil, err } composeConfigFile := types.ConfigFile{ @@ -22,14 +25,19 @@ func IsValidStackFile(stackFileContent []byte, securitySettings *portainer.Endpo Environment: map[string]string{}, } - composeConfig, err := loader.Load(composeConfigDetails, func(options *loader.Options) { + return loader.Load(composeConfigDetails, func(options *loader.Options) { options.SkipValidation = true options.SkipInterpolation = true }) - if err != nil { - return err - } +} +func IsValidStackFileAdapter(securitySettings *portainer.EndpointSecuritySettings) func(*types.Config) error { + return func(composeConfig *types.Config) error { + return IsValidStackFile(composeConfig, securitySettings) + } +} + +func IsValidStackFile(composeConfig *types.Config, securitySettings *portainer.EndpointSecuritySettings) error { for key := range composeConfig.Services { service := composeConfig.Services[key] if !securitySettings.AllowBindMountsForRegularUsers { @@ -64,17 +72,42 @@ func IsValidStackFile(stackFileContent []byte, securitySettings *portainer.Endpo return nil } -func ValidateStackFiles(stack *portainer.Stack, securitySettings *portainer.EndpointSecuritySettings, fileService portainer.FileService) error { +func ValidateStackFiles(stack *portainer.Stack, isValidFn func(content *types.Config) error, fileService portainer.FileService) error { for _, file := range GetStackFilePaths(stack, false) { stackContent, err := fileService.GetFileContent(stack.ProjectPath, file) if err != nil { return errors.Wrap(err, "failed to get stack file content") } - err = IsValidStackFile(stackContent, securitySettings) + composeConfig, err := loadComposeConfig(stackContent) + if err != nil { + return err + } + + err = isValidFn(composeConfig) if err != nil { return errors.Wrap(err, "stack config file is invalid") } } + + return nil +} + +func IsValidBuildContext(composeConfig *types.Config) error { + for key := range composeConfig.Services { + service := composeConfig.Services[key] + + if strings.HasPrefix(service.Build.Context, "/") || strings.Contains(service.Build.Context, "..") { + return errors.New("invalid build context") + } + + driveLetter, _, ok := strings.Cut(service.Build.Context, ":") + driveLetter = strings.ToUpper(driveLetter) + + if ok && len(driveLetter) == 1 && driveLetter >= "A" && driveLetter <= "Z" { + return errors.New("invalid build context") + } + } + return nil } diff --git a/pkg/libstack/compose/internal/composeplugin/composeplugin.go b/pkg/libstack/compose/internal/composeplugin/composeplugin.go index ca98426c0..3e81b270d 100644 --- a/pkg/libstack/compose/internal/composeplugin/composeplugin.go +++ b/pkg/libstack/compose/internal/composeplugin/composeplugin.go @@ -126,10 +126,7 @@ func (wrapper *PluginWrapper) command(command composeCommand, options libstack.O command.WithHost(options.Host) } - var stderr bytes.Buffer - - args := []string{} - args = append(args, command.ToArgs()...) + args := append([]string{}, command.ToArgs()...) cmd := exec.Command(program, args...) cmd.Dir = options.WorkingDir @@ -150,7 +147,8 @@ func (wrapper *PluginWrapper) command(command composeCommand, options libstack.O Interface("env", cmd.Env). Msg("run command") - cmd.Stderr = &stderr + stderr := &bytes.Buffer{} + cmd.Stderr = stderr output, err := cmd.Output() if err != nil {