From fb6f6738d9b619e2516b1eccda6edcbf38efc766 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Mon, 7 Oct 2019 16:12:21 +1300 Subject: [PATCH] fix(api): prevent the use of bind mounts in stacks if setting enabled (#3232) --- .../handler/stacks/create_compose_stack.go | 30 ++++++++++++++- api/http/handler/stacks/create_swarm_stack.go | 28 +++++++++++++- api/http/handler/stacks/handler.go | 1 + api/http/handler/stacks/stack_create.go | 38 +++++++++++++++++++ api/http/server.go | 1 + .../stacks/create/createStackController.js | 4 +- 6 files changed, 97 insertions(+), 5 deletions(-) diff --git a/api/http/handler/stacks/create_compose_stack.go b/api/http/handler/stacks/create_compose_stack.go index 66c9d91cb..7cae40cf9 100644 --- a/api/http/handler/stacks/create_compose_stack.go +++ b/api/http/handler/stacks/create_compose_stack.go @@ -1,7 +1,9 @@ package stacks import ( + "errors" "net/http" + "path" "strconv" "strings" @@ -238,7 +240,7 @@ func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter, } stackFolder := strconv.Itoa(int(stack.ID)) - projectPath, err := handler.FileService.StoreStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent)) + projectPath, err := handler.FileService.StoreStackFileFromBytes(stackFolder, stack.EntryPoint, payload.StackFileContent) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist Compose file on disk", err} } @@ -271,6 +273,7 @@ type composeStackDeploymentConfig struct { endpoint *portainer.Endpoint dockerhub *portainer.DockerHub registries []portainer.Registry + isAdmin bool } func (handler *Handler) createComposeDeployConfig(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint) (*composeStackDeploymentConfig, *httperror.HandlerError) { @@ -295,6 +298,7 @@ func (handler *Handler) createComposeDeployConfig(r *http.Request, stack *portai endpoint: endpoint, dockerhub: dockerhub, registries: filteredRegistries, + isAdmin: securityContext.IsAdmin, } return config, nil @@ -306,12 +310,34 @@ func (handler *Handler) createComposeDeployConfig(r *http.Request, stack *portai // clean it. Hence the use of the mutex. // We should contribute to libcompose to support authentication without using the config.json file. func (handler *Handler) deployComposeStack(config *composeStackDeploymentConfig) error { + settings, err := handler.SettingsService.Settings() + if err != nil { + return err + } + + if !settings.AllowBindMountsForRegularUsers && !config.isAdmin { + composeFilePath := path.Join(config.stack.ProjectPath, config.stack.EntryPoint) + + stackContent, err := handler.FileService.GetFileContent(composeFilePath) + if err != nil { + return err + } + + valid, err := handler.isValidStackFile(stackContent) + if err != nil { + return err + } + if !valid { + return errors.New("bind-mount disabled for non administrator users") + } + } + handler.stackCreationMutex.Lock() defer handler.stackCreationMutex.Unlock() handler.SwarmStackManager.Login(config.dockerhub, config.registries, config.endpoint) - err := handler.ComposeStackManager.Up(config.stack, config.endpoint) + err = handler.ComposeStackManager.Up(config.stack, config.endpoint) if err != nil { return err } diff --git a/api/http/handler/stacks/create_swarm_stack.go b/api/http/handler/stacks/create_swarm_stack.go index 0832111e0..4210cab0e 100644 --- a/api/http/handler/stacks/create_swarm_stack.go +++ b/api/http/handler/stacks/create_swarm_stack.go @@ -1,7 +1,9 @@ package stacks import ( + "errors" "net/http" + "path" "strconv" "strings" @@ -290,6 +292,7 @@ type swarmStackDeploymentConfig struct { dockerhub *portainer.DockerHub registries []portainer.Registry prune bool + isAdmin bool } func (handler *Handler) createSwarmDeployConfig(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint, prune bool) (*swarmStackDeploymentConfig, *httperror.HandlerError) { @@ -315,18 +318,41 @@ func (handler *Handler) createSwarmDeployConfig(r *http.Request, stack *portaine dockerhub: dockerhub, registries: filteredRegistries, prune: prune, + isAdmin: securityContext.IsAdmin, } return config, nil } func (handler *Handler) deploySwarmStack(config *swarmStackDeploymentConfig) error { + settings, err := handler.SettingsService.Settings() + if err != nil { + return err + } + + if !settings.AllowBindMountsForRegularUsers && !config.isAdmin { + composeFilePath := path.Join(config.stack.ProjectPath, config.stack.EntryPoint) + + stackContent, err := handler.FileService.GetFileContent(composeFilePath) + if err != nil { + return err + } + + valid, err := handler.isValidStackFile(stackContent) + if err != nil { + return err + } + if !valid { + return errors.New("bind-mount disabled for non administrator users") + } + } + handler.stackCreationMutex.Lock() defer handler.stackCreationMutex.Unlock() handler.SwarmStackManager.Login(config.dockerhub, config.registries, config.endpoint) - err := handler.SwarmStackManager.Deploy(config.stack, config.prune, config.endpoint) + err = handler.SwarmStackManager.Deploy(config.stack, config.prune, config.endpoint) if err != nil { return err } diff --git a/api/http/handler/stacks/handler.go b/api/http/handler/stacks/handler.go index cdfe9ea0d..b81270543 100644 --- a/api/http/handler/stacks/handler.go +++ b/api/http/handler/stacks/handler.go @@ -25,6 +25,7 @@ type Handler struct { DockerHubService portainer.DockerHubService SwarmStackManager portainer.SwarmStackManager ComposeStackManager portainer.ComposeStackManager + SettingsService portainer.SettingsService } // NewHandler creates a handler to manage stack operations. diff --git a/api/http/handler/stacks/stack_create.go b/api/http/handler/stacks/stack_create.go index 0d1adae53..aba79482c 100644 --- a/api/http/handler/stacks/stack_create.go +++ b/api/http/handler/stacks/stack_create.go @@ -5,6 +5,9 @@ import ( "log" "net/http" + "github.com/docker/cli/cli/compose/types" + + "github.com/docker/cli/cli/compose/loader" httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/portainer/api" @@ -87,3 +90,38 @@ func (handler *Handler) createSwarmStack(w http.ResponseWriter, r *http.Request, return &httperror.HandlerError{http.StatusBadRequest, "Invalid value for query parameter: method. Value must be one of: string, repository or file", errors.New(request.ErrInvalidQueryParameter)} } + +func (handler *Handler) isValidStackFile(stackFileContent []byte) (bool, error) { + composeConfigYAML, err := loader.ParseYAML(stackFileContent) + if err != nil { + return false, err + } + + composeConfigFile := types.ConfigFile{ + Config: composeConfigYAML, + } + + composeConfigDetails := types.ConfigDetails{ + ConfigFiles: []types.ConfigFile{composeConfigFile}, + Environment: map[string]string{}, + } + + composeConfig, err := loader.Load(composeConfigDetails, func(options *loader.Options) { + options.SkipValidation = true + options.SkipInterpolation = true + }) + if err != nil { + return false, err + } + + for key := range composeConfig.Services { + service := composeConfig.Services[key] + for _, volume := range service.Volumes { + if volume.Type == "bind" { + return false, nil + } + } + } + + return true, nil +} diff --git a/api/http/server.go b/api/http/server.go index 42d3751db..a2a48cfe8 100644 --- a/api/http/server.go +++ b/api/http/server.go @@ -207,6 +207,7 @@ func (server *Server) Start() error { stackHandler.GitService = server.GitService stackHandler.RegistryService = server.RegistryService stackHandler.DockerHubService = server.DockerHubService + stackHandler.SettingsService = server.SettingsService var tagHandler = tags.NewHandler(requestBouncer) tagHandler.TagService = server.TagService diff --git a/app/portainer/views/stacks/create/createStackController.js b/app/portainer/views/stacks/create/createStackController.js index 6ecd6519a..337edaf58 100644 --- a/app/portainer/views/stacks/create/createStackController.js +++ b/app/portainer/views/stacks/create/createStackController.js @@ -1,4 +1,4 @@ -import { AccessControlFormData } from '../../../components/accessControlForm/porAccessControlFormModel'; +import {AccessControlFormData} from '../../../components/accessControlForm/porAccessControlFormModel'; angular.module('portainer.app') .controller('CreateStackController', ['$scope', '$state', 'StackService', 'Authentication', 'Notifications', 'FormValidator', 'ResourceControlService', 'FormHelper', 'EndpointProvider', @@ -124,7 +124,7 @@ function ($scope, $state, StackService, Authentication, Notifications, FormValid $state.go('portainer.stacks'); }) .catch(function error(err) { - Notifications.warning('Deployment error', type === 1 ? err.err.data.err : err.data.err); + Notifications.error('Deployment error', err, 'Unable to deploy stack'); }) .finally(function final() { $scope.state.actionInProgress = false;