# Portainer has an endpoint security bypass via Swarm service create/update

Portainer's Swarm service create/update routes skip EndpointSecuritySettings checks, letting non-admin users add CAP_SYS_ADMIN, unconfined seccomp, or host bind mounts to gain host root.

- Payout: Research
- Severity: critical
- Program: Portainer
- Found by: JohannesLks, route2shell
- Disclosed: 2026-05-14
- CVE: CVE-2026-44849
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-5fxq-qcf3-244w
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-5fxq-qcf3-244w
- Tags: area:cloud domain:cloud domain:infra impact:internal-access impact:privesc kind:research platform:ghsa sev:critical tech:docker technique:source-review vuln:access-control vuln:auth-bypass vuln:misconfiguration vuln:privilege-escalation

**Target:** Docker Swarm service create/update and volume create API proxied by Portainer
**Root cause:** The partial PartialService struct only declares Mounts[].Type, so CapabilityAdd, CapabilityDrop, Sysctls, and Privileges are never parsed or validated; the service update route dispatches to restrictedResourceOperation without calling fetchEndpointSecuritySettings(). The bind-mount check reads only top-level Type, ignoring VolumeOptions.DriverConfig.Options.
**Steps:**
1. Authenticate as a non-admin user with RBAC access to a Swarm endpoint where AllowContainerCapabilitiesForRegularUsers / AllowSecurityOptForRegularUsers / AllowBindMountsForRegularUsers restrict standard users.
2. POST /services/create with `CapabilityAdd: ["SYS_ADMIN","NET_ADMIN","SYS_PTRACE"]` or `Privileges.Seccomp.Mode: "unconfined"` — only the Mounts check runs (1/7).
3. Alternatively create a benign service that passes ownership checks, then POST /services/{id}/update with `CapabilityAdd: ["ALL"]` plus a bind mount of `/` (0/7 checks).
4. Scale to one replica and access the host filesystem from the container, e.g. `chroot /host`.
5. Bind-mount variant: send a mount with `Type: "volume"` and `VolumeOptions.DriverConfig.Options: {type: "none", o: "bind", device: "<host path>"}`, or the same payload to POST /volumes/create, which has no AllowBindMountsForRegularUsers check.
**How found:** Compared which EndpointSecuritySettings checks fire on the container-creation path versus the Swarm service API: service create applied only 1 of 7 and service update 0 of 7. Reading the partial struct revealed undeclared fields (CapabilityAdd, Sysctls, Privileges) were silently dropped from validation, and that Mounts only inspected Type, prompting the volume-driver bind test.
**Exploitation:** A restricted Swarm-endpoint user creates or updates a service with elevated capabilities, unconfined seccomp/AppArmor, arbitrary sysctls, or a host bind mount (directly or via local volume driver options), scales it to one replica, and reads/writes the host filesystem — equivalent to root on the Swarm manager host.
**Escalation:** Chain CapabilityAdd:["ALL"] with a bind mount of `/` (or /var/run/docker.sock, SSH keys, Portainer DB) to convert a restricted account into host root; the volume-driver variant bypasses the bind-mount check even where it exists.
**Insight:** When a proxy validates a JSON body by unmarshalling into a partial struct, any field not declared is invisible to the check but still forwarded to the backend — enumerate the backend's full schema and diff it against the partial struct. Also check sibling routes (update vs create, /volumes/create) for the same check, and remember Docker's local volume driver accepts `o: bind` to emulate a bind mount.

