mirror of https://github.com/portainer/portainer
fix(service): webhook vulnerability for passing an invalid image tag EE-2121 (#6269)
* fix(service): webhook vulnerability for passing an invalid image tagpull/6488/head
parent
dfb0ba9efe
commit
a9406764ee
|
@ -1,11 +1,13 @@
|
|||
package webhooks
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/docker"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
@ -13,6 +15,7 @@ import (
|
|||
// Handler is the HTTP handler used to handle webhook operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
requestBouncer *security.RequestBouncer
|
||||
DataStore dataservices.DataStore
|
||||
DockerClientFactory *docker.ClientFactory
|
||||
}
|
||||
|
@ -20,7 +23,8 @@ type Handler struct {
|
|||
// NewHandler creates a handler to manage webhooks operations.
|
||||
func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
Router: mux.NewRouter(),
|
||||
requestBouncer: bouncer,
|
||||
}
|
||||
h.Handle("/webhooks",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.webhookCreate))).Methods(http.MethodPost)
|
||||
|
@ -34,3 +38,43 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
|||
bouncer.PublicAccess(httperror.LoggerHandler(h.webhookExecute))).Methods(http.MethodPost)
|
||||
return h
|
||||
}
|
||||
|
||||
func (handler *Handler) checkResourceAccess(r *http.Request, resourceID string, resourceControlType portainer.ResourceControlType) *httperror.HandlerError {
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve user info from request context", Err: err}
|
||||
}
|
||||
// non-admins
|
||||
rc, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(resourceID, resourceControlType)
|
||||
if rc == nil || err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve a resource control associated to the resource", Err: err}
|
||||
}
|
||||
userTeamIDs := make([]portainer.TeamID, 0)
|
||||
for _, membership := range securityContext.UserMemberships {
|
||||
userTeamIDs = append(userTeamIDs, membership.TeamID)
|
||||
}
|
||||
canAccess := authorization.UserCanAccessResource(securityContext.UserID, userTeamIDs, rc)
|
||||
if !canAccess {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "This operation is disabled for non-admin users and unassigned access users"}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) checkAuthorization(r *http.Request, endpoint *portainer.Endpoint, authorizations []portainer.Authorization) (bool, *httperror.HandlerError) {
|
||||
err := handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
return false, &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Permission denied to access environment", Err: err}
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return false, &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve user info from request context", Err: err}
|
||||
}
|
||||
|
||||
authService := authorization.NewService(handler.DataStore)
|
||||
isAdminOrAuthorized, err := authService.UserIsAdminOrAuthorized(securityContext.UserID, endpoint.ID, authorizations)
|
||||
if err != nil {
|
||||
return false, &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to get user authorizations", Err: err}
|
||||
}
|
||||
return isAdminOrAuthorized, nil
|
||||
}
|
||||
|
|
|
@ -64,6 +64,15 @@ func (handler *Handler) webhookCreate(w http.ResponseWriter, r *http.Request) *h
|
|||
|
||||
endpointID := portainer.EndpointID(payload.EndpointID)
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve user info from request context", Err: err}
|
||||
}
|
||||
|
||||
if !securityContext.IsAdmin {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Not authorized to create a webhook", Err: errors.New("not authorized to create a webhook")}
|
||||
}
|
||||
|
||||
if payload.RegistryID != 0 {
|
||||
tokenData, err := security.RetrieveTokenData(r)
|
||||
if err != nil {
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package webhooks
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
|
@ -25,6 +27,15 @@ func (handler *Handler) webhookDelete(w http.ResponseWriter, r *http.Request) *h
|
|||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid webhook id", err}
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve user info from request context", Err: err}
|
||||
}
|
||||
|
||||
if !securityContext.IsAdmin {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Not authorized to delete a webhook", Err: errors.New("not authorized to delete a webhook")}
|
||||
}
|
||||
|
||||
err = handler.DataStore.Webhook().DeleteWebhook(portainer.WebhookID(id))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the webhook from the database", err}
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"github.com/portainer/portainer/api/internal/registryutils"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
|
@ -111,7 +112,15 @@ func (handler *Handler) executeServiceWebhook(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if imageTag != "" {
|
||||
rc, err := dockerClient.ImagePull(context.Background(), service.Spec.TaskTemplate.ContainerSpec.Image, dockertypes.ImagePullOptions{RegistryAuth: serviceUpdateOptions.EncodedRegistryAuth})
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Error pulling image with the specified tag", Err: err}
|
||||
}
|
||||
defer func(rc io.ReadCloser) {
|
||||
_ = rc.Close()
|
||||
}(rc)
|
||||
}
|
||||
_, err = dockerClient.ServiceUpdate(context.Background(), resourceID, service.Version, service.Spec, serviceUpdateOptions)
|
||||
|
||||
if err != nil {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package webhooks
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
|
@ -33,6 +34,14 @@ func (handler *Handler) webhookList(w http.ResponseWriter, r *http.Request) *htt
|
|||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: filters", err}
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve user info from request context", Err: err}
|
||||
}
|
||||
if !securityContext.IsAdmin {
|
||||
return response.JSON(w, []portainer.Webhook{})
|
||||
}
|
||||
|
||||
webhooks, err := handler.DataStore.Webhook().Webhooks()
|
||||
webhooks = filterWebhooks(webhooks, &filters)
|
||||
if err != nil {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package webhooks
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
|
@ -53,6 +54,15 @@ func (handler *Handler) webhookUpdate(w http.ResponseWriter, r *http.Request) *h
|
|||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a webhooks with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve user info from request context", Err: err}
|
||||
}
|
||||
|
||||
if !securityContext.IsAdmin {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Not authorized to update a webhook", Err: errors.New("not authorized to update a webhook")}
|
||||
}
|
||||
|
||||
if payload.RegistryID != 0 {
|
||||
tokenData, err := security.RetrieveTokenData(r)
|
||||
if err != nil {
|
||||
|
|
|
@ -603,3 +603,21 @@ func getAuthorizationsFromRoles(roleIdentifiers []portainer.RoleID, roles []port
|
|||
|
||||
return authorizations
|
||||
}
|
||||
|
||||
func (service *Service) UserIsAdminOrAuthorized(userID portainer.UserID, endpointID portainer.EndpointID, authorizations []portainer.Authorization) (bool, error) {
|
||||
user, err := service.dataStore.User().User(userID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if user.Role == portainer.AdministratorRole {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
for _, authorization := range authorizations {
|
||||
_, authorized := user.EndpointAuthorizations[endpointID][authorization]
|
||||
if authorized {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
|
|
@ -95,7 +95,7 @@
|
|||
</div>
|
||||
<!-- !port-mapping -->
|
||||
<!-- create-webhook -->
|
||||
<div ng-if="endpoint.Type !== 4">
|
||||
<div ng-if="endpoint.Type !== 4 && isAdmin">
|
||||
<div class="col-sm-12 form-section-title"> Webhooks </div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
|
|
|
@ -74,7 +74,7 @@
|
|||
<td>Image</td>
|
||||
<td>{{ service.Image }}</td>
|
||||
</tr>
|
||||
<tr ng-if="applicationState.endpoint.type !== 4">
|
||||
<tr ng-if="isAdmin && applicationState.endpoint.type !== 4">
|
||||
<td colspan="{{ webhookURL ? '1' : '2' }}">
|
||||
Service webhook
|
||||
<portainer-tooltip
|
||||
|
|
Loading…
Reference in New Issue