← Notes

CVE-2026-44849: Portainer Swarm Endpoint Security Bypass

Overview

CVECVE-2026-44849
AdvisoryGHSA-5fxq-qcf3-244w
Componentapi/http/proxy/factory/docker/services.go and api/http/proxy/factory/docker/transport.go
AffectedPortainer 2.33.x before 2.33.8, 2.39.x before 2.39.2, and 2.40.x before 2.41.0
FixedPortainer 2.33.8, 2.39.2, and 2.41.0
ClassCWE-862 (Missing Authorization)
CVSS v4.09.4 (Critical) -- CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H
Found byLukas Johannes Moeller (@JohannesLks) and route2shell

TL;DR

Portainer lets administrators restrict what non-admin users may run on Docker endpoints: privileged containers, host namespaces, Linux capabilities, sysctls, seccomp/AppArmor settings, device mappings, and bind mounts. Those restrictions were enforced on the normal container creation path, but not consistently on Docker Swarm services. Service creation parsed only a narrow subset of the request body, and service update reused a generic ownership check without inspecting the new service spec at all. A non-admin user who already had access to a Swarm endpoint could therefore create or update services with settings the administrator had explicitly disabled for regular users.

Root Cause

Portainer enforces seven EndpointSecuritySettings restrictions on the regular container-create path. On the Swarm service API, coverage was incomplete since the feature was introduced:

Path Checks applied Gap
POST /services/create 1 of 7 (bind mounts only) CapabilityAdd, CapabilityDrop, Sysctls, Privileges not parsed
POST /services/{id}/update 0 of 7 Generic ownership check only; no body inspection, no fetchEndpointSecuritySettings()

The service-create proxy path used a partial struct to inspect only mount type information before forwarding the original request to Docker:

type PartialService struct {
    TaskTemplate struct {
        ContainerSpec struct {
            Mounts []struct {
                Type string
            }
        }
    }
}

Fields such as CapabilityAdd, CapabilityDrop, Sysctls, and Privileges were absent from that validation view. They survived in the forwarded JSON body but were invisible to Portainer's policy check.

The service-update path was broader: requests matching /services/*/* were dispatched to a generic restricted-resource operation. That function verified that the user was allowed to operate on the service resource, but it did not fetch EndpointSecuritySettings, parse the submitted service spec, or compare the requested container settings against the endpoint policy.

Bind-Style Volume Variant

The bind-mount check also trusted only the top-level mount type. Docker's local volume driver can materialize a bind-equivalent mount through driver options, while the request still appears as Type: "volume" at the top level:

Mounts: [{
    Type: "volume",
    VolumeOptions: {
        DriverConfig: {
            Name: "local",
            Options: {
                type: "none",
                o: "bind",
                device: "<host path>"
            }
        }
    }
}]

Because the old partial struct did not include VolumeOptions.DriverConfig.Options, this shape bypassed the Type != "bind" test and reached the Docker daemon unchanged. The same policy gap existed for direct /volumes/create requests, which did not apply the regular-user bind-mount restriction on the affected branches.

Impact

  • Required position: authenticated non-admin Portainer user with access to a Docker Swarm endpoint through Portainer RBAC.
  • Bypassed controls: capability allow/drop restrictions, sysctl restrictions, seccomp/AppArmor restrictions, and bind-mount restrictions on Swarm service create/update paths.
  • Practical effect: workloads could be launched or modified with host-impacting container settings that the endpoint administrator had disabled for regular users.
  • In combination, elevated capabilities plus host filesystem access can collapse the intended boundary between a restricted Portainer account and the Swarm host.

Admins and users without Swarm endpoint access were not affected by this specific path. The bug is still a security-boundary issue because the vulnerable role is exactly the user group EndpointSecuritySettings are meant to constrain.

Fix

The fix brings the Swarm paths back under the same policy model as regular container creation:

  • extend the service-create validation view to include the security-sensitive fields that Docker accepts in ContainerSpec;
  • inspect service-update request bodies instead of relying only on resource ownership checks;
  • treat local-driver type=none / o=bind volume options as bind-equivalent for policy purposes;
  • apply volume body restrictions to direct volume creation as well.

The important invariant is that Portainer must validate the same semantic container configuration that the Docker daemon will execute. Partial JSON views are safe only when every security-relevant alias and nested representation is included.

Credits

Reported independently by Lukas Johannes Moeller (@JohannesLks) and route2shell through Portainer's coordinated disclosure process.