Portainer
Products
1- 9 CVEs
Recent CVEs
9| CVE | Sev | Risk | CVSS | EPSS | KEV | Published | Description |
|---|---|---|---|---|---|---|---|
| CVE-2026-44849 | cri | 0.59 | — | — | May 14, 2026 | ## Summary Portainer enforces seven `EndpointSecuritySettings` restrictions that administrators configure to restrict the container configurations non-admin users can launch: **privileged mode**, **host PID namespace**, **device mapping**, **capabilities**, **sysctls**, **security-opt (Seccomp / AppArmor)**, and **bind mounts**. The vulnerability is exposed when a non-admin Portainer user (Standard User role, or any role granted endpoint-level access) has been given access to a Docker Swarm endpoint via Portainer RBAC. Admins and users without Swarm endpoint access are not affected. These restrictions are enforced on the standard container creation path, but several of them are not applied on the Docker Swarm service API: - `POST /services/create` — **1 of 7** checks applied. `CapabilityAdd`, `CapabilityDrop`, `Sysctls`, and `Privileges` (Seccomp / AppArmor) are not parsed from the request body and are forwarded to the Docker daemon without validation. - `POST /services/{id}/update` — **0 of 7** checks applied. The route dispatches to the generic `restrictedResourceOperation`, which validates RBAC ownership but does not inspect the request body or call `fetchEndpointSecuritySettings()`. The `EndpointSecuritySettings` checks apply when the administrator has configured any of `AllowContainerCapabilitiesForRegularUsers`, `AllowSysctlSettingForRegularUsers`, `AllowSecurityOptForRegularUsers`, or `AllowBindMountsForRegularUsers` to restrict standard users. A regular user with access to a Docker Swarm endpoint can: - Create a service with `CapabilityAdd: ["SYS_ADMIN", "NET_ADMIN", "SYS_PTRACE", …]` or `Privileges.Seccomp.Mode: "unconfined"`. - Create a benign service that passes ownership checks, then **update** it to add `CapabilityAdd: ["ALL"]` plus a bind mount of `/`, scale to one replica, and access the host filesystem from the running container (e.g. via `chroot /host`). In addition, the partial `Mounts[]` struct used by the bind-mount check inspects only the top-level `Type` field. A mount with `Type: "volume"` and `VolumeOptions.DriverConfig.Options: {type: "none", o: "bind", device: "<host path>"}` is forwarded to the Docker daemon unchanged; the local volume driver then materialises it as a bind-equivalent mount, bypassing `AllowBindMountsForRegularUsers`. The same field path is accepted by the standalone `POST /volumes/create` endpoint, which never had any `AllowBindMountsForRegularUsers` check on any branch. This undermines the administrator's configured security policy on Swarm-enabled endpoints. ## Affected Versions The vulnerability exists in every Portainer release with Docker Swarm support — the service-creation path has never checked `CapabilityAdd`, `CapabilityDrop`, `Sysctls`, or `Privileges`, and the service-update path has never performed any `EndpointSecuritySettings` validation. The `VolumeOptions.DriverConfig` field has never been parsed by the partial service struct on any branch, so the volume-driver-bind variant (service create/update and direct `/volumes/create`) shares the same affected range. Fixes are included in the next release of each supported branch: | Branch | First vulnerable | Fixed in | |---------------------|------------------|------------| | 2.33.x (LTS) | 2.33.0 | **2.33.8** | | 2.39.x (LTS) | 2.39.0 | **2.39.2** | | 2.40.x (STS) | 2.40.0 | **2.41.0** | Portainer LTS branches receive fixes for 6 months plus a 3-month overlap after the next LTS ships. STS releases are supported only until the next STS ships — the 2.40.x STS line ends with the 2.41.0 release. All releases **prior to 2.33.0 are end-of-life** and will not receive a fix; users on EOL versions should upgrade to a supported LTS branch. ## Workarounds Administrators who cannot immediately upgrade can reduce exposure with the following measures. None of these replaces the fix. - **Temporarily revoke Swarm endpoint access for non-admin users** via Portainer RBAC until the patched release is deployed. This eliminates the attack surface without service disruption for administrators. - **Segregate manager and worker nodes** with placement constraints so user workloads do not run on manager nodes. This limits the exposure of the Swarm control plane if the bypass is exploited against a worker. - **Block creation of local-driver volumes that use `type: none` / `o: bind`** on untrusted endpoints via a daemon-side allowlist. This closes the volume-driver-bind variant until the patched release is deployed. ## Affected Code ### Service creation — only `Mounts` inspected (1/7) ```go // api/http/proxy/factory/docker/services.go (pre-fix) type PartialService struct { TaskTemplate struct { ContainerSpec struct { Mounts []struct { Type string } } } } ``` `CapabilityAdd`, `CapabilityDrop`, `Sysctls`, and `Privileges` are not declared in the struct, so `json.Unmarshal` does not include them in the validated view. The request body is then forwarded to the Docker daemon without those fields being checked. ### Service update — no inspection (0/7) ```go // api/http/proxy/factory/docker/transport.go (pre-fix) if match, _ := path.Match("/services/*/*", requestPath); match { serviceID := path.Base(path.Dir(requestPath)) // ... no body inspection, no call to fetchEndpointSecuritySettings ... return transport.restrictedResourceOperation( request, serviceID, serviceID, portainer.ServiceResourceControl, false, ) } ``` `fetchEndpointSecuritySettings()` is called in three places in the codebase: container creation, service creation (bind-mount check only), and volume browsing. Service update is not among them. ### Bind-mount check — driver options ignored ```go // api/http/proxy/factory/docker/services.go (pre-fix — partial Mounts struct) Mounts []struct { Type string // only this field was read } ``` Because `VolumeOptions.DriverConfig.Options` is not declared in the partial struct, a mount of `Type: "volume"` passes the `Type != "bind"` check and is forwarded to the daemon. The local volume driver treats `{type: "none", o: "bind", device: "<host path>"}` as a bind-equivalent mount, so the check is bypassed. The fix extends the partial struct to carry `VolumeOptions.DriverConfig.Options map[string]string`, rejects service create/update requests where that map declares a bind-style driver, and adds a new `CheckVolumeBodyRestrictions` invocation on `POST /volumes/create` (which previously had no `AllowBindMountsForRegularUsers` check on any branch). ## Impact An authenticated, non-admin Portainer user with access to any Docker Swarm-enabled endpoint can configure a service with: - **Elevated Linux capabilities** including `CAP_SYS_ADMIN`, `CAP_NET_ADMIN`, `CAP_SYS_PTRACE`, or `ALL` — not restricted by `AllowContainerCapabilitiesForRegularUsers`. - **Disabled syscall filtering** via `Privileges.Seccomp.Mode: "unconfined"` — not restricted by `AllowSecurityOptForRegularUsers`. - **Disabled AppArmor confinement** via `Privileges.AppArmor.Mode: "disabled"` — not restricted by `AllowSecurityOptForRegularUsers`. - **Arbitrary sysctl values** inside the container namespace — not restricted by `AllowSysctlSettingForRegularUsers`. - **Bind mounts of any host path**, including `/`, `/var/run/docker.sock`, SSH keys, or Portainer's own database — not restricted by `AllowBindMountsForRegularUsers`. - **Bind-mount-equivalent host filesystem access via volume driver options** — a `Type: "volume"` mount whose `VolumeOptions.DriverConfig.Options` describe a local-driver bind, or a direct `POST /volumes/create` with the same payload, yields the same capability as a direct bind and is not restricted by `AllowBindMountsForRegularUsers`. In combination (e.g. `CapabilityAdd:["ALL"]` + bind mount of `/`), this gives a user access equivalent to root on the Swarm manager host from a restricted account, overriding the administrator's security policy. ## Timeline - `2026-03-12` — route2shell privately discloses the volume-driver local-bind variant. - `2026-04-05` — JohannesLks disclosure of the Swarm service create/update bypass - `2026-04-18` — Fix merged to develop. - `2026-04-29` — 2.41.0 released. - `2026-05-07` — 2.39.2-LTS and 2.33.8-LTS released. ## Credit - **route2shell** — disclosure of the volume-driver local-bind variant on both Swarm service creation/update and the standalone `/volumes/create` endpoint. - **JohannesLks** — independent disclosure of the Swarm service create/update bypass | |
| CVE-2026-44848 | cri | 0.59 | — | — | May 14, 2026 | ## Summary Portainer enforces Role-Based Access Control (RBAC) on top of the Docker API. The proxy layer routes incoming Docker API requests to per-resource handlers (containers, images, services, volumes, etc.) that apply authorization checks. The Docker plugin management endpoints (`/plugins/*`) were not registered with a handler, so standard users with endpoint access could call privileged plugin operations — including installing and enabling plugins — directly against the underlying Docker daemon. The vulnerability is exposed when a non-admin Portainer user (Standard User role, or any role granted endpoint-level access) has been given access to a Docker endpoint via Portainer RBAC. Administrators and users without Docker endpoint access are not affected. A regular user with access to a Docker endpoint can: - Pull an arbitrary plugin from any registry via `POST /plugins/pull`. - Grant it the privileges it requests, including `CAP_SYS_ADMIN` and host-path mounts. - Enable the plugin via `POST /plugins/{name}/enable`, at which point Docker runs the plugin with root privileges on the host. Docker plugins execute as root on the host and can request arbitrary host capabilities and mounts. Enabling a crafted plugin gives the user access to the host filesystem and equivalent to root on the Docker host. ## Severity **Critical** — CVSS 9.4 `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` **CWE-862** — Missing Authorization ## Affected Versions The vulnerability exists in every Portainer release where the Docker API proxy uses the prefix-allowlist routing model — `/plugins` has never been in the allowlist, and the fall-through path has never applied authorization. Fixes are included in the next release of each supported branch: | Branch | First vulnerable | Fixed in | |---------------------|------------------|------------| | 2.33.x (LTS) | 2.33.0 | **2.33.8** | | 2.39.x (LTS) | 2.39.0 | **2.39.2** | | 2.40.x (STS) | 2.40.0 | **2.41.0** | Portainer LTS branches receive fixes for 6 months plus a 3-month overlap after the next LTS ships. STS releases are supported only until the next STS ships — the 2.40.x STS line ends with the 2.41.0 release. All releases **prior to 2.33.0 are end-of-life** and will not receive a fix; users on EOL versions should upgrade to a supported LTS branch. ## Workarounds Administrators who cannot immediately upgrade can reduce exposure by temporarily **revoking Docker endpoint access for non-admin users** via Portainer RBAC until the patched release is deployed. This eliminates the attack surface without disruption for administrators. This does not replace the fix. ## Affected Code ```go // api/http/proxy/factory/docker/transport.go (pre-fix) var prefixProxyFuncMap = map[string]func(...){ "build": ..., "configs": ..., "containers": ..., "images": ..., "networks": ..., "nodes": ..., "secrets": ..., "services": ..., "swarm": ..., "tasks": ..., "v2": ..., "volumes": ..., } func (transport *Transport) ProxyDockerRequest(request *http.Request) (*http.Response, error) { // ... prefix := strings.Split(strings.TrimPrefix(unversionedPath, "/"), "/")[0] if proxyFunc := prefixProxyFuncMap[prefix]; proxyFunc != nil { return proxyFunc(transport, request, unversionedPath) // authorized } return transport.executeDockerRequest(request) // forwarded without authorization } ``` `/plugins` is not in `prefixProxyFuncMap`, so requests to plugin endpoints fall through to `executeDockerRequest` and are forwarded to the Docker daemon without any Portainer-side authorization check. ## Impact An authenticated, non-admin Portainer user with access to any Docker-enabled endpoint can: - Install and enable arbitrary Docker plugins from any registry. - Execute plugin code with root privileges on the Docker host (including declaring `CAP_SYS_ADMIN` and host-path mounts). - Read and modify files on the host filesystem from a restricted account, overriding the administrator's security policy. ## Timeline - 2026-03-16: Reported via GitHub Security Advisory by **ikkebr**. - 2026-04-20: Fix merged to `develop`, `release/2.39`, and `release/2.33`. - 2026-04-29: 2.41.0 released. - 2026-05-07: 2.39.2-LTS and 2.33.8-LTS released. ## Credit - **ikkebr** — identified and reported the proxy allowlist bypass affecting the Docker plugin management endpoints. | |
| CVE-2026-44883 | hig | 0.45 | — | — | May 14, 2026 | ## Summary Portainer's authentication middleware accepts JWT bearer tokens passed as the `?token=<JWT>` URL query parameter on any authenticated API endpoint, in addition to the standard `Authorization: Bearer` header. URLs are recorded in reverse-proxy access logs, browser history, and HTTP `Referer` headers on outbound navigation, so any JWT passed this way can be harvested by anyone with access to those logs or by an external site the user subsequently visits. A leaked token grants the full privileges of the user it was issued to, until the token expires (default 8 hours, configurable). The `?token=` parameter was used by Portainer's browser-based container attach, exec, and pod shell features, so any user with exec or attach rights on a container was exposed — not only administrators. ## Severity **High** Attack complexity is High because exploitation depends on the attacker obtaining a leaked token from a log, referer, or shared URL. Once obtained, a leaked token grants the privileges of the user it was issued to; for administrator tokens this compromises confidentiality, integrity, and availability of Portainer itself and of every Docker/Kubernetes environment it manages — container exec and stack deployment make host-level compromise reachable, so subsequent-system impact is also High. ## Affected Versions Query-parameter token acceptance has existed since JWT authentication was introduced in Portainer. Fixes are included in the following releases: | Branch | First vulnerable | Fixed in | |---------------------|------------------|------------| | 2.33.x (LTS) | 2.33.0 | **2.33.8** | | 2.39.x (LTS) | 2.39.0 | **2.39.2** | | 2.40.x (STS) | all prior | **2.41.0** | Portainer releases prior to 2.33.0 are end-of-life and will not receive a fix. Users on EOL versions should upgrade to a supported LTS branch. ## Workarounds Administrators who cannot immediately upgrade can reduce exposure by: - **Stripping `?token=` at the reverse proxy.** A rewrite rule in nginx, Traefik, or equivalent that removes the `token` query parameter before the request reaches Portainer blocks the query-parameter auth path entirely. Container exec and interactive shells rely on the query-parameter token for WebSocket upgrade and will stop working until the patched release is deployed. - **Auditing existing logs.** Search reverse-proxy access logs and application logs for `?token=` or `&token=` occurrences and treat any captured JWT as compromised. Resetting the affected user's password invalidates their sessions; reducing the JWT session timeout in Portainer settings shortens the exposure window for tokens already issued. - **Administrator hygiene.** Do not share Portainer URLs that contain `?token=` in chat, email, or tickets, and avoid navigating to external sites from within the Portainer UI on unpatched instances — the `Referer` header will carry the token. None of these replace the fix. ## Affected Code Pre-fix, `extractBearerToken` in `api/http/security/bouncer.go` read the JWT from the `token` query parameter before falling back to the `Authorization` header. The `query.Del("token")` call scrubs the parameter from `r.URL.RawQuery` on the way through Portainer, but by that point the original URL has already been recorded by any upstream reverse proxy, access logger, or browser. ```go func extractBearerToken(r *http.Request) (string, bool) { query := r.URL.Query() token := query.Get("token") if token != "" { query.Del("token") r.URL.RawQuery = query.Encode() return token, true } tokens, ok := r.Header[jwtTokenHeader] if !ok || len(tokens) == 0 { return "", false } // ... } ``` The fix removes the query-parameter path entirely. Authenticated requests now carry the JWT via the `Authorization` header for API clients, or via the `portainer_api_key` HttpOnly cookie for the browser UI — cookies are sent automatically on same-origin WebSocket upgrade requests, so the browser-based container attach, exec, and pod shell features continue to work without exposing the token in the URL. The WebSocket handlers that previously documented `?token=` as a required query parameter have been updated to match. ## Impact - **Token leakage to infrastructure.** Intermediate systems that observe the request URL — reverse proxies, load balancers, access logs, WAFs, and corporate network monitoring — capture the full JWT in plaintext. - **Token leakage via the browser.** URLs containing `?token=` are recorded in browser history and forwarded in the `Referer` header on any outbound navigation from the Portainer UI. - **Account takeover.** Anyone with access to a leaked JWT acts as the authenticated user for the remainder of the token's validity, without needing the password. If the leaked token belongs to an administrator, the attacker gains full API access including user management, container exec, and stack deployment. - **Reach beyond Portainer.** Container exec with an administrator JWT reaches the host filesystem of managed environments and can be used to execute commands on those hosts. ## Timeline - 2026-03-06: Reported via GitHub Security Advisory by **scanpwn**. - 2026-04-14: Fix merged to `develop`. - 2026-04-29: 2.41.0 released. - 2026-05-07: 2.39.2-LTS and 2.33.8-LTS released. ## Credit - **scanpwn** — identified and reported the query-parameter JWT acceptance and the resulting token-leakage vectors. | |
| CVE-2026-44882 | hig | 0.45 | — | — | May 14, 2026 | ## Summary Portainer proxies requests to Kubernetes clusters through a middleware layer (`kubeClientMiddleware`) that validates the requesting user's token before forwarding traffic to the cluster. When `security.RetrieveTokenData` returned an error, the middleware wrote an HTTP 403 response but was missing a `return` statement — execution continued into the handler with a nil `tokenData` value. The Kubernetes endpoints sit behind Portainer's outer `AuthenticatedAccess` bouncer, so an attacker requires a valid Portainer session. However, a user whose secondary token validation fails in `kubeClientMiddleware` — for example a user without permission to access a given Kubernetes endpoint — would have their request forwarded to the cluster anyway, bypassing the authorization check. The same defect was present in both the CE and EE codebases. ## Severity **High** **CWE-863** — Incorrect Authorization Privilege required is Low — any valid Portainer session is sufficient to reach the middleware. Once the authorization outcome is bypassed, the attacker can read and modify Kubernetes resources on the target endpoint that their role should not permit — confidentiality and integrity impact are both High. No availability impact is introduced directly. ## Affected Versions The missing `return` statement has been present since Kubernetes proxy support was introduced. | Branch | First vulnerable | Fixed in | |--------------|------------------|------------| | 2.33.x (LTS) | 2.33.0 | **2.33.8** | Portainer 2.39.0 and later are not affected — the fix was present from the initial 2.39.0 release. All releases prior to 2.33.0 are end-of-life and will not receive a fix; users on EOL versions should upgrade to a supported release. ## Workarounds There is no configuration change that prevents the bypass directly. Administrators who cannot immediately upgrade can reduce exposure by: - **Restricting Kubernetes endpoint access.** Remove Portainer access to Kubernetes endpoints for users who do not require it. A user without endpoint access cannot reach `kubeClientMiddleware`. - **Auditing Kubernetes RBAC.** Ensure the service account Portainer uses to proxy cluster requests carries least-privilege RBAC permissions — this limits the blast radius if the bypass is exploited. Neither of these replaces the fix. ## Affected Code `kubeClientMiddleware` in `api/http/handler/kubernetes/handler.go` wrote the error response but did not return, allowing execution to continue with nil `tokenData`: ```go // api/http/handler/kubernetes/handler.go (pre-fix — CE and EE) tokenData, err := security.RetrieveTokenData(r) if err != nil { httperror.WriteError(w, http.StatusForbidden, "permission denied to access the environment", err) // missing return — tokenData is nil, execution continues } // tokenData.ID dereferenced on the next line: _, ok := handler.KubernetesClientFactory.GetProxyKubeClient( strconv.Itoa(endpointID), strconv.Itoa(int(tokenData.ID))) ``` The fix adds a single return after the WriteError call in both CE and EE: ```go // post-fix if err != nil { httperror.WriteError(w, http.StatusForbidden, "permission denied to access the environment", err) return } ``` ## Impact - Kubernetes authorization bypass. A low-privileged Portainer user can reach Kubernetes API endpoints on environments their role does not permit, with the proxy client of the legitimate session used as the vehicle. - Cluster resource access. Depending on the service account permissions Portainer holds on the cluster, the attacker can read or modify namespaced resources — including pods, secrets, config maps, and deployments. - Potential for lateral movement. Kubernetes secrets readable through this path may contain credentials for other services within the cluster or the broader infrastructure. ## Timeline - 2026-02-16: Fix merged to develop. - 2026-02-25: 2.39.0 released with fix. - 2026-05-07: 2.33.8 released with backport fix. | |
| CVE-2026-44881 | hig | 0.45 | — | — | May 14, 2026 | ## Summary Portainer supports deploying stacks from Git repositories. When a Git-backed stack is created or updated, Portainer clones the repository using `go-git` v5, which translates Git blob entries with mode `0o120000` (symlink) into real OS symlinks on the host filesystem via `os.Symlink`. The only entry blocked from becoming a symlink is `.gitmodules`; every other path — including `docker-compose.yml`, which Portainer treats as the stack entry point — is created as a symlink without validation. Portainer's `GET /api/stacks/{id}/file` endpoint then reads the stack entry point with `os.ReadFile`, which follows OS symlinks transparently. A repository containing `docker-compose.yml` as a symlink to an arbitrary filesystem path (for example `/etc/passwd` or a mounted Kubernetes service account token) causes the symlink target's contents to be returned verbatim in the HTTP response. Any authenticated user with rights to create or update a Git-backed stack — the default configuration in Portainer CE — can read arbitrary files accessible to the Portainer process. The issue is amplified by Git-stack auto-update: an attacker can create a stack from a legitimate repository, pass initial review, and later push a commit that replaces `docker-compose.yml` with a symlink; the file read is then triggered on the next scheduled update cycle with no further interaction required. ## Severity **High** Attack complexity is Low: the attacker needs only the ability to host a Git repository and the default-granted permission to create a Git-backed stack. Privilege required is Low in typical CE deployments, where non-admin users can manage their own stacks; administrators retain the same attack surface regardless of the setting. Impact on confidentiality is High — the Portainer process commonly runs as root (required for Docker socket access), so arbitrary file read includes `/etc/shadow`, Kubernetes service account tokens, Docker secrets, environment variables, and the Portainer database itself. Integrity and availability are not directly affected, but the leaked contents (service account tokens, registry credentials, database session keys) frequently enable onward compromise of the host and managed environments. ## Affected Versions The vulnerability exists in every Portainer release since the introduction of Git-based stack deployment support — Git-backed stacks have always performed an unrestricted `go-git` checkout and subsequently read the entry-point file through `os.ReadFile` without resolving symlinks. Fixes are included in the following releases: | Branch | First vulnerable | Fixed in | |--------------|------------------|------------| | 2.33.x (LTS) | 2.33.0 | **2.33.8** | | 2.39.x (LTS) | 2.39.0 | **2.39.2** | | 2.40.x (STS) | all prior | **2.41.0** | Portainer releases prior to 2.33.0 are end-of-life and will not receive a fix. Users on EOL versions should upgrade to a supported LTS branch. ## Workarounds Administrators who cannot immediately upgrade can reduce exposure by: - **Restricting who can create Git-backed stacks.** Disable **Allow non-admin users to manage their stacks** in environment settings so that only administrators can submit a Git repository URL. This reduces the attack to an administrator-only surface but does not remove it. - **Avoiding untrusted repositories.** Do not deploy Git-backed stacks from repositories you do not control or review, and do not grant stack-management rights to users who can supply an arbitrary repository URL. - **Disabling auto-update on existing stacks.** Auto-update re-clones the repository on a schedule, which allows a repository that was safe at creation time to later become malicious. Disabling auto-update removes the deferred-exploitation path. - **Auditing existing stack working directories.** Search project paths under `/data/compose/` (or your configured data directory) for symlink entries — `find /data/compose -type l` — and treat any unexpected results as potential evidence of past exploitation. None of these replace the fix. ## Affected Code The vulnerability is the combination of two primitives. `go-git` translates Git symlink entries into OS symlinks unconditionally (except `.gitmodules`): ```go // go-git v5 — Worktree.checkoutFileSymlink func (w *Worktree) checkoutFileSymlink(f *object.File) (err error) { if strings.EqualFold(f.Name, gitmodulesFile) { return ErrGitModulesSymlink } // ... reads blob content as raw bytes ... err = w.Filesystem.Symlink(string(bytes), f.Name) return } ``` Relative symlink targets (`../../etc/passwd`) are passed through to `os.Symlink` as-is and escape the worktree at OS resolution time. (Absolute targets are chrooted to the worktree by `go-billy`'s `ChrootHelper.Symlink` and are not useful to the attacker.) On the read side, `GetFileContent` in `api/filesystem/filesystem.go` applies lexical path containment but not symlink resolution: ```go func (service *Service) GetFileContent(trustedRoot, filePath string) ([]byte, error) { content, err := os.ReadFile(JoinPaths(trustedRoot, filePath)) return content, err } ``` `JoinPaths` prevents `../` traversal in the input string but does not call `filepath.EvalSymlinks`, so a symlink already written to the project path resolves through `os.ReadFile` to its ultimate target. The fix wraps the `go-billy` filesystem used by the Git checkout with a custom `noSymlinkFS` type whose `Symlink()` method returns `ErrSymlinkDetected`, causing the clone to fail rather than write any OS symlink. Git trees that would otherwise produce a symlink entry are rejected at checkout time, closing the primary attack path. On the 2.33.x and 2.39.x branches the fix also hardens `GetFileContent` to call `filepath.EvalSymlinks` and verify the resolved path remains inside the trusted root, providing a second layer of defence against any future regression in Git-checkout handling. ## Impact - **Arbitrary file read as the Portainer process.** Any file readable by the Portainer process — typically root in containerized deployments — can be returned through the stack file endpoint. Common targets include `/etc/shadow`, `/root/.ssh/*`, `/proc/self/environ`, and the Portainer BoltDB (`portainer.db`) which contains all user password hashes, API tokens, and agent credentials. - **Kubernetes service account token exposure.** Portainer running on Kubernetes has its cluster service account token mounted at `/var/run/secrets/kubernetes.io/serviceaccount/token`; reading it grants the attacker the Portainer pod's cluster API access. - **Docker Swarm secret exposure.** Secrets mounted into the Portainer container at `/run/secrets/` (for example the initial admin password in Swarm deployments) are readable with the same mechanism. - **Onward compromise.** Leaked service tokens, registry credentials, and database contents frequently enable authenticated access to managed Docker/Kubernetes environments, container registries, and Portainer itself under other users' identities. - **Deferred exploitation via auto-update.** A repository that passes initial review at stack creation can be mutated afterwards; the malicious commit takes effect on the next auto-update cycle without user interaction. ## Timeline - 2026-03-20: Reported via GitHub Security Advisory by **b-hermes**. - 2026-04-18: Fix merged to `develop`. - 2026-04-29: 2.41.0 released with fix. - 2026-05-07: 2.33.8, 2.39.2, released with fix. ## Credit - **b-hermes** — identified the Git symlink injection primitive, traced the end-to-end chain through `GetFileContent`, and provided a fully validated proof-of-concept. | |
| CVE-2026-44850 | hig | 0.45 | — | — | May 14, 2026 | ## Summary Portainer offers an environment-level **Disable bind mounts for non-administrators** security setting that blocks regular users from binding host paths into containers they create through the Portainer-mediated Docker API. The check that enforces this setting only inspected the legacy `HostConfig.Binds` array on the container-create proxy and never looked at the equivalent `HostConfig.Mounts` array. Any authenticated user with rights to create containers on a Docker environment where the restriction is enabled could submit a `bind`-typed entry under `HostConfig.Mounts` and mount any host path into their container. The two fields are interchangeable on the Docker daemon — both produce real bind mounts at runtime — so a check that inspects only one is functionally equivalent to no check at all. The same primitive is correctly enforced on Swarm service create against `TaskTemplate.ContainerSpec.Mounts`; the gap was specific to the `POST /containers/create` proxy path. Exploitation requires a regular user with container-create rights on an environment that has the restriction enabled. Such a user can mount any host path read-write or read-only into a container they own and use the resulting view of the host filesystem to read or write anything the Docker daemon's user can — typically `root`. Bind-mount restriction is the primary defence against host filesystem exposure on shared environments where regular users are otherwise permitted to deploy containers. ## Severity **High** The vulnerability is exploitable over the network with low attack complexity, no attack requirement, and no user interaction. It requires a low-privilege authenticated session — any regular user with container-create rights on an environment where the bind-mount restriction is enabled. The vulnerable system (the Portainer container-create proxy) suffers a confidentiality and integrity breach by virtue of the bypass itself, but the dominant impact is on the subsequent system: the Docker host's filesystem and any container running alongside the attacker's. This is a restriction bypass rather than a cross-authority escalation — the user already had container-create rights, and the bind-mount restriction is a defence-in-depth control on top of that capability — which is the reason the rating is held at High rather than promoted to Critical despite the host-level reach. ## Affected Versions The vulnerability has existed since the `AllowBindMountsForRegularUsers` security setting was introduced. The `HostConfig.Mounts` field has never been inspected by the container-create proxy on any release line. Fixes are included in the following releases: | Branch | First vulnerable | Fixed in | |--------------|------------------|------------| | 2.33.x (LTS) | 2.33.0 | **2.33.8** | | 2.39.x (LTS) | 2.39.0 | **2.39.2** | | 2.41.x (STS) | all prior | **2.41.0** | Portainer releases prior to 2.33.0 are end-of-life and will not receive a fix. Users on EOL versions should upgrade to a supported LTS branch. ## Workarounds Administrators who cannot immediately upgrade can reduce exposure by: - **Revoke container-create rights from non-administrator accounts on affected environments.** If the bind-mount restriction is being relied on as a hard guarantee, audit which non-administrator accounts have container-create rights on environments where it is set, and downgrade those accounts to roles that lack container-create until the patched release is deployed. Stack and service deployment that depends on container-create will stop working for those users until the patched release is in place. - **Audit recent container creations for `HostConfig.Mounts` of `Type: bind` from non-admin Portainer users.** Inspect Docker daemon logs and `docker inspect` output on affected environments. Any non-admin-created container with a bind-typed `Mounts` entry should be treated as a potential incident. - **Segregate tenants by environment.** Where the per-environment toggle was being used to share an environment between tenants of different trust levels, splitting the workloads onto separate environments is a stronger control than the toggle and remains in place after upgrade. None of these replace the fix. ## Affected Code The enforcement lives in `decorateContainerCreationOperation` in `package/server-ce/api/http/proxy/factory/docker/containers.go`. The `PartialContainer` struct used to deserialise the request body for inspection only contained `HostConfig.Binds`: ``` // package/server-ce/api/http/proxy/factory/docker/containers.go type PartialContainer struct { HostConfig struct { Privileged bool `json:"Privileged"` PidMode string `json:"PidMode"` Devices []any `json:"Devices"` Sysctls map[string]any `json:"Sysctls"` CapAdd []string `json:"CapAdd"` CapDrop []string `json:"CapDrop"` Binds []string `json:"Binds"` } `json:"HostConfig"` } if !securitySettings.AllowBindMountsForRegularUsers && len(partialContainer.HostConfig.Binds) > 0 { for _, bind := range partialContainer.HostConfig.Binds { if strings.HasPrefix(bind, "/") { return forbiddenResponse, ErrBindMountsForbidden } } } ``` The fix adds a `Mounts` field to `PartialContainer` and a parallel check that rejects any entry whose `Type` equals `bind`, mirroring the existing logic on the Swarm service-create proxy. The container-update path is unaffected — the Docker daemon does not accept mount changes via container update — and Swarm service create was already covered. Compose-stack deployment is not in scope of this advisory; the bind-mount restriction is a daemon-mediated container-create control, and Compose deployment runs Docker through a separate path that is not currently subject to the same restriction. The same change applies cleanly on each LTS branch — the surrounding code shape on `release/2.33` and `release/2.39` is identical to develop on the points the patch touches, so the LTS backports are byte-equivalent additions of the `Mounts` field and the parallel check. ## Impact A regular user who has been explicitly restricted from using bind mounts can bypass the restriction and: - **Read or write any path on the Docker host filesystem.** The mount runs as the daemon user (typically `root`), so any path is reachable. Sensitive examples include `/etc/shadow`, host SSH keys under `/root/.ssh` and `/home/*/.ssh`, and TLS material under `/etc/docker`. - **Compromise other containers on the same host.** The host's `/var/lib/docker` (or equivalent) is reachable from within the bound mount, exposing the layers, volumes, and live state of every container the daemon manages. - **Reach the Docker socket.** Mounting `/var/run/docker.sock` into the attacker's container hands them full Docker API access on the host, regardless of any authorisation enforced by Portainer above the proxy. - **Write persistence to the host.** Without `ReadOnly`, the attacker can drop SSH keys into `authorized_keys`, install systemd units, or modify cron, achieving persistence outside of any container the daemon supervises. The bind-mount restriction was the primary defence against this class of host exposure for non-administrator container creators; bypassing it removes the only enforcement point above the daemon for tenants who were granted container-create rights. ## Timeline - 2026-03-04: Reported via GitHub Security Advisory by **offensiveee** (Assaf Alassaf). - 2026-03-04 – 2026-04-17: Six further independent reports of the same primitive received via GitHub Security Advisory (alexwaira, ffulbtech, Proscan-one, jeroengui, AyushParkara, marduc812) and consolidated against this advisory. - 2026-04-18: Fix merged to `develop`. - 2026-06-04: Backports merged to `release/2.33` and `release/2.39`. - 2026-04-29: 2.41.0 released with fix. - 2026-05-07: 2.33.8, 2.39.2, released with fix. ## Credit - **offensiveee** (Assaf Alassaf) — initial report identifying the `HostConfig.Mounts` bypass on container create and the divergence from the Swarm service-create check. - **alexwaira** — independently reported the same `HostConfig.Mounts` bypass on container create. - **ffulbtech** — independently reported the same `HostConfig.Mounts` bypass on container create. - **Proscan-one** — independently reported the same `HostConfig.Mounts` bypass on container create. - **jeroengui** — independently reported the same `HostConfig.Mounts` bypass on container create. - **AyushParkara** — independently reported the same `HostConfig.Mounts` bypass on container create. - **marduc812** — independently reported the same `HostConfig.Mounts` bypass on container create. | |
| CVE-2025-49593 | Med | 0.37 | 6.8 | 0.00 | Jun 17, 2025 | Portainer Community Edition is a lightweight service delivery platform for containerized applications that can be used to manage Docker, Swarm, Kubernetes and ACI environments. Prior to STS version 2.31.0 and LTS version 2.27.7, if a Portainer administrator can be convinced to register a malicious container registry, or an existing container registry can be taken over, HTTP Headers (including registry authentication credentials or Portainer session tokens) may be leaked to that registry. This issue has been patched in STS version 2.31.0 and LTS version 2.27.7. | |
| CVE-2026-44884 | 0.00 | — | — | May 14, 2026 | ## Summary A missing authorization vulnerability in the Custom Template file endpoint (`GET /api/custom_templates/{id}/file`) allows any authenticated user to read the file content of any custom template by enumerating sequential integer IDs, bypassing Resource Control access restrictions. Template files may contain environment-specific values such as connection strings, API tokens, or registry credentials that administrators would not expect standard users to read. ## Severity **Medium** **CWE-862** — Missing Authorization Exploitation requires an authenticated user account and at least one custom template to exist. Template files are returned verbatim and may contain embedded credentials. ## Affected Versions The vulnerability exists in every Portainer release since custom templates were introduced — the `customTemplateFile` handler has never performed an authorization check. Fixes are included in the following releases: | Branch | First vulnerable | Fixed in | |---------------------|------------------|------------| | 2.33.x (LTS) | 2.33.0 | **2.33.8** | | 2.39.x (LTS) | 2.39.0 | **2.39.1** | Portainer 2.40.0 and later are not affected — the fix was already on `develop` when the 2.40.x STS line branched. Portainer LTS branches receive fixes for 6 months plus a 3-month overlap after the next LTS ships. All releases **prior to 2.33.0 are end-of-life** and will not receive a fix; users on EOL versions should upgrade to a supported release. ## Workarounds There is no runtime configuration that blocks the vulnerable endpoint directly. Administrators who cannot immediately upgrade can reduce exposure by: - **Avoiding storing secrets in custom templates** until the patched release is deployed. Move sensitive configuration values to Portainer environment variables or an external secret store. - **Reviewing existing custom templates** for embedded secrets. Assume any secret previously stored in a custom template on an unpatched instance has been exposed to every authenticated user and rotate accordingly. Neither replaces the fix. ## Affected Code The `customTemplateFile` handler in `api/http/handler/customtemplates/customtemplate_file.go` (lines 30-53) retrieves a custom template by its numeric ID and returns the file content without performing any authorization check. All other custom template endpoints properly verify access: | Endpoint | Method | Authorization Check | |----------|--------|-------------------| | `/api/custom_templates/{id}` | GET (inspect) | `userCanEditTemplate()` + `UserCanAccessResource()` | | `/api/custom_templates/{id}` | PUT (update) | `userCanEditTemplate()` | | `/api/custom_templates/{id}` | DELETE | `userCanEditTemplate()` | | `/api/custom_templates` | GET (list) | `FilterAuthorizedCustomTemplates()` | | **`/api/custom_templates/{id}/file`** | **GET** | **None** | **Vulnerable code** (`customtemplate_file.go:30-53`): ```go func (handler *Handler) customTemplateFile(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { customTemplateID, _ := request.RetrieveNumericRouteVariableValue(r, "id") customTemplate, _ := handler.DataStore.CustomTemplate().Read(portainer.CustomTemplateID(customTemplateID)) // NO AUTHORIZATION CHECK fileContent, _ := handler.FileService.GetFileContent(customTemplate.ProjectPath, entryPath) return response.JSON(w, &fileResponse{FileContent: string(fileContent)}) } ``` **Secure reference** (`customtemplate_inspect.go:50-75`): ```go canEdit := userCanEditTemplate(customTemplate, securityContext) hasAccess := authorization.UserCanAccessResource(securityContext.UserID, teamIDs, resourceControl) if !canEdit && !hasAccess { return httperror.Forbidden("Access denied to resource", httperrors.ErrResourceAccessDenied) } ``` ## Impact Any authenticated user (including the lowest-privilege standard user) can read the file content of every custom template in the instance. Custom templates commonly contain Docker Compose configuration, which may include environment-specific secrets such as database connection strings, API tokens, or registry credentials. ## Timeline - 2026-02-11: Reported via GitHub Security Advisory by **duddnr0615k**. - 2026-03-04: Fix merged to `develop` and cherry-picked to `release/2.39`. - 2026-03-19: 2.39.1 released with fix. - 2026-03-25: 2.40.0 released with fix already present from branch cut. - 2026-05-07: 2.33.8 released. ## Credit - **duddnr0615k** — identified and reported the missing authorization check on the custom template file endpoint. | ||
| CVE-2026-44885 | 0.00 | — | — | May 14, 2026 | ### Summary Portainer's backup restore feature accepts a `.tar.gz` archive and extracts it to a target directory on the server. The extraction function (`ExtractTarGz` in `api/archive/targz.go`) constructed output paths using `filepath.Clean(filepath.Join(outputDirPath, header.Name))`. This combination does not prevent directory traversal — a tar entry named `../../etc/cron.d/evil` resolves to a path outside the extraction root, so a crafted archive can write files to arbitrary locations on the server filesystem. ## Severity **Medium** **CWE-22** — Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') Exploitation requires administrator access to Portainer's backup restore endpoint. An administrator who is deceived into restoring a malicious archive, or whose credentials are compromised, can use this path to write files outside the Portainer data directory. ## Affected Versions The vulnerability exists in every Portainer release prior to 2.39.0 — `ExtractTarGz` has used `filepath.Clean(filepath.Join())` since it was introduced. The fix shipped with 2.39.0 (patched on `develop` before the 2.39 branch cut); 2.34.x–2.38.x STS releases are also affected but are end-of-life and will not receive a fix. | Branch | First vulnerable | Fixed in | |--------------|------------------|------------| | 2.33.x (LTS) | 2.33.0 | **2.33.8** | Portainer 2.39.0 and later are not affected — the fix was present from the initial 2.39.0 release. All releases prior to 2.33.0 are end-of-life and will not receive a fix; users on EOL versions should upgrade to a supported release. ## Workarounds Administrators who cannot immediately upgrade should: - **Only restore archives from trusted sources.** Do not restore archives received from untrusted parties or transmitted over unencrypted channels. - **Use backup encryption.** Portainer's optional backup encryption requires the correct passphrase to decrypt before extraction; an attacker without the passphrase cannot craft a valid encrypted archive. Neither of these replaces the fix. ## Affected Code `ExtractTarGz` in `api/archive/targz.go` constructed output paths without safe containment: ```go // api/archive/targz.go (pre-fix) case tar.TypeReg: p := filepath.Clean(filepath.Join(outputDirPath, header.Name)) ``` filepath.Join resolves ../ components lexically and filepath.Clean normalises the result, but neither verifies the final path remains inside outputDirPath. The fix replaces this with filesystem.JoinPaths, which forces all path components to be relative to the trusted root: ```go // api/archive/targz.go (post-fix) case tar.TypeReg: p := filesystem.JoinPaths(outputDirPath, header.Name) ``` ### Impact - Arbitrary file write at any path accessible to the Portainer process (typically root in containerised deployments), overriding filesystem boundaries of the data directory. - Potential host persistence by writing to cron directories, SSH authorised key files, or executable paths, depending on how the container is configured and what host paths are accessible. The practical severity is reduced because exploitation requires administrative privileges within Portainer. ## Timeline - 2026-02-16: Fix merged to develop ([#1875](https://github.com/portainer/portainer-suite/pull/1875)). - 2026-02-25: 2.39.0 released with fix. - 2026-05-07: 2.33.8 released with backport fix. ### Credits Reported by [Kolega](https://kolega.ai). |
- risk 0.59cvss —epss —
## Summary Portainer enforces seven `EndpointSecuritySettings` restrictions that administrators configure to restrict the container configurations non-admin users can launch: **privileged mode**, **host PID namespace**, **device mapping**, **capabilities**, **sysctls**, **security-opt (Seccomp / AppArmor)**, and **bind mounts**. The vulnerability is exposed when a non-admin Portainer user (Standard User role, or any role granted endpoint-level access) has been given access to a Docker Swarm endpoint via Portainer RBAC. Admins and users without Swarm endpoint access are not affected. These restrictions are enforced on the standard container creation path, but several of them are not applied on the Docker Swarm service API: - `POST /services/create` — **1 of 7** checks applied. `CapabilityAdd`, `CapabilityDrop`, `Sysctls`, and `Privileges` (Seccomp / AppArmor) are not parsed from the request body and are forwarded to the Docker daemon without validation. - `POST /services/{id}/update` — **0 of 7** checks applied. The route dispatches to the generic `restrictedResourceOperation`, which validates RBAC ownership but does not inspect the request body or call `fetchEndpointSecuritySettings()`. The `EndpointSecuritySettings` checks apply when the administrator has configured any of `AllowContainerCapabilitiesForRegularUsers`, `AllowSysctlSettingForRegularUsers`, `AllowSecurityOptForRegularUsers`, or `AllowBindMountsForRegularUsers` to restrict standard users. A regular user with access to a Docker Swarm endpoint can: - Create a service with `CapabilityAdd: ["SYS_ADMIN", "NET_ADMIN", "SYS_PTRACE", …]` or `Privileges.Seccomp.Mode: "unconfined"`. - Create a benign service that passes ownership checks, then **update** it to add `CapabilityAdd: ["ALL"]` plus a bind mount of `/`, scale to one replica, and access the host filesystem from the running container (e.g. via `chroot /host`). In addition, the partial `Mounts[]` struct used by the bind-mount check inspects only the top-level `Type` field. A mount with `Type: "volume"` and `VolumeOptions.DriverConfig.Options: {type: "none", o: "bind", device: "<host path>"}` is forwarded to the Docker daemon unchanged; the local volume driver then materialises it as a bind-equivalent mount, bypassing `AllowBindMountsForRegularUsers`. The same field path is accepted by the standalone `POST /volumes/create` endpoint, which never had any `AllowBindMountsForRegularUsers` check on any branch. This undermines the administrator's configured security policy on Swarm-enabled endpoints. ## Affected Versions The vulnerability exists in every Portainer release with Docker Swarm support — the service-creation path has never checked `CapabilityAdd`, `CapabilityDrop`, `Sysctls`, or `Privileges`, and the service-update path has never performed any `EndpointSecuritySettings` validation. The `VolumeOptions.DriverConfig` field has never been parsed by the partial service struct on any branch, so the volume-driver-bind variant (service create/update and direct `/volumes/create`) shares the same affected range. Fixes are included in the next release of each supported branch: | Branch | First vulnerable | Fixed in | |---------------------|------------------|------------| | 2.33.x (LTS) | 2.33.0 | **2.33.8** | | 2.39.x (LTS) | 2.39.0 | **2.39.2** | | 2.40.x (STS) | 2.40.0 | **2.41.0** | Portainer LTS branches receive fixes for 6 months plus a 3-month overlap after the next LTS ships. STS releases are supported only until the next STS ships — the 2.40.x STS line ends with the 2.41.0 release. All releases **prior to 2.33.0 are end-of-life** and will not receive a fix; users on EOL versions should upgrade to a supported LTS branch. ## Workarounds Administrators who cannot immediately upgrade can reduce exposure with the following measures. None of these replaces the fix. - **Temporarily revoke Swarm endpoint access for non-admin users** via Portainer RBAC until the patched release is deployed. This eliminates the attack surface without service disruption for administrators. - **Segregate manager and worker nodes** with placement constraints so user workloads do not run on manager nodes. This limits the exposure of the Swarm control plane if the bypass is exploited against a worker. - **Block creation of local-driver volumes that use `type: none` / `o: bind`** on untrusted endpoints via a daemon-side allowlist. This closes the volume-driver-bind variant until the patched release is deployed. ## Affected Code ### Service creation — only `Mounts` inspected (1/7) ```go // api/http/proxy/factory/docker/services.go (pre-fix) type PartialService struct { TaskTemplate struct { ContainerSpec struct { Mounts []struct { Type string } } } } ``` `CapabilityAdd`, `CapabilityDrop`, `Sysctls`, and `Privileges` are not declared in the struct, so `json.Unmarshal` does not include them in the validated view. The request body is then forwarded to the Docker daemon without those fields being checked. ### Service update — no inspection (0/7) ```go // api/http/proxy/factory/docker/transport.go (pre-fix) if match, _ := path.Match("/services/*/*", requestPath); match { serviceID := path.Base(path.Dir(requestPath)) // ... no body inspection, no call to fetchEndpointSecuritySettings ... return transport.restrictedResourceOperation( request, serviceID, serviceID, portainer.ServiceResourceControl, false, ) } ``` `fetchEndpointSecuritySettings()` is called in three places in the codebase: container creation, service creation (bind-mount check only), and volume browsing. Service update is not among them. ### Bind-mount check — driver options ignored ```go // api/http/proxy/factory/docker/services.go (pre-fix — partial Mounts struct) Mounts []struct { Type string // only this field was read } ``` Because `VolumeOptions.DriverConfig.Options` is not declared in the partial struct, a mount of `Type: "volume"` passes the `Type != "bind"` check and is forwarded to the daemon. The local volume driver treats `{type: "none", o: "bind", device: "<host path>"}` as a bind-equivalent mount, so the check is bypassed. The fix extends the partial struct to carry `VolumeOptions.DriverConfig.Options map[string]string`, rejects service create/update requests where that map declares a bind-style driver, and adds a new `CheckVolumeBodyRestrictions` invocation on `POST /volumes/create` (which previously had no `AllowBindMountsForRegularUsers` check on any branch). ## Impact An authenticated, non-admin Portainer user with access to any Docker Swarm-enabled endpoint can configure a service with: - **Elevated Linux capabilities** including `CAP_SYS_ADMIN`, `CAP_NET_ADMIN`, `CAP_SYS_PTRACE`, or `ALL` — not restricted by `AllowContainerCapabilitiesForRegularUsers`. - **Disabled syscall filtering** via `Privileges.Seccomp.Mode: "unconfined"` — not restricted by `AllowSecurityOptForRegularUsers`. - **Disabled AppArmor confinement** via `Privileges.AppArmor.Mode: "disabled"` — not restricted by `AllowSecurityOptForRegularUsers`. - **Arbitrary sysctl values** inside the container namespace — not restricted by `AllowSysctlSettingForRegularUsers`. - **Bind mounts of any host path**, including `/`, `/var/run/docker.sock`, SSH keys, or Portainer's own database — not restricted by `AllowBindMountsForRegularUsers`. - **Bind-mount-equivalent host filesystem access via volume driver options** — a `Type: "volume"` mount whose `VolumeOptions.DriverConfig.Options` describe a local-driver bind, or a direct `POST /volumes/create` with the same payload, yields the same capability as a direct bind and is not restricted by `AllowBindMountsForRegularUsers`. In combination (e.g. `CapabilityAdd:["ALL"]` + bind mount of `/`), this gives a user access equivalent to root on the Swarm manager host from a restricted account, overriding the administrator's security policy. ## Timeline - `2026-03-12` — route2shell privately discloses the volume-driver local-bind variant. - `2026-04-05` — JohannesLks disclosure of the Swarm service create/update bypass - `2026-04-18` — Fix merged to develop. - `2026-04-29` — 2.41.0 released. - `2026-05-07` — 2.39.2-LTS and 2.33.8-LTS released. ## Credit - **route2shell** — disclosure of the volume-driver local-bind variant on both Swarm service creation/update and the standalone `/volumes/create` endpoint. - **JohannesLks** — independent disclosure of the Swarm service create/update bypass
- risk 0.59cvss —epss —
## Summary Portainer enforces Role-Based Access Control (RBAC) on top of the Docker API. The proxy layer routes incoming Docker API requests to per-resource handlers (containers, images, services, volumes, etc.) that apply authorization checks. The Docker plugin management endpoints (`/plugins/*`) were not registered with a handler, so standard users with endpoint access could call privileged plugin operations — including installing and enabling plugins — directly against the underlying Docker daemon. The vulnerability is exposed when a non-admin Portainer user (Standard User role, or any role granted endpoint-level access) has been given access to a Docker endpoint via Portainer RBAC. Administrators and users without Docker endpoint access are not affected. A regular user with access to a Docker endpoint can: - Pull an arbitrary plugin from any registry via `POST /plugins/pull`. - Grant it the privileges it requests, including `CAP_SYS_ADMIN` and host-path mounts. - Enable the plugin via `POST /plugins/{name}/enable`, at which point Docker runs the plugin with root privileges on the host. Docker plugins execute as root on the host and can request arbitrary host capabilities and mounts. Enabling a crafted plugin gives the user access to the host filesystem and equivalent to root on the Docker host. ## Severity **Critical** — CVSS 9.4 `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` **CWE-862** — Missing Authorization ## Affected Versions The vulnerability exists in every Portainer release where the Docker API proxy uses the prefix-allowlist routing model — `/plugins` has never been in the allowlist, and the fall-through path has never applied authorization. Fixes are included in the next release of each supported branch: | Branch | First vulnerable | Fixed in | |---------------------|------------------|------------| | 2.33.x (LTS) | 2.33.0 | **2.33.8** | | 2.39.x (LTS) | 2.39.0 | **2.39.2** | | 2.40.x (STS) | 2.40.0 | **2.41.0** | Portainer LTS branches receive fixes for 6 months plus a 3-month overlap after the next LTS ships. STS releases are supported only until the next STS ships — the 2.40.x STS line ends with the 2.41.0 release. All releases **prior to 2.33.0 are end-of-life** and will not receive a fix; users on EOL versions should upgrade to a supported LTS branch. ## Workarounds Administrators who cannot immediately upgrade can reduce exposure by temporarily **revoking Docker endpoint access for non-admin users** via Portainer RBAC until the patched release is deployed. This eliminates the attack surface without disruption for administrators. This does not replace the fix. ## Affected Code ```go // api/http/proxy/factory/docker/transport.go (pre-fix) var prefixProxyFuncMap = map[string]func(...){ "build": ..., "configs": ..., "containers": ..., "images": ..., "networks": ..., "nodes": ..., "secrets": ..., "services": ..., "swarm": ..., "tasks": ..., "v2": ..., "volumes": ..., } func (transport *Transport) ProxyDockerRequest(request *http.Request) (*http.Response, error) { // ... prefix := strings.Split(strings.TrimPrefix(unversionedPath, "/"), "/")[0] if proxyFunc := prefixProxyFuncMap[prefix]; proxyFunc != nil { return proxyFunc(transport, request, unversionedPath) // authorized } return transport.executeDockerRequest(request) // forwarded without authorization } ``` `/plugins` is not in `prefixProxyFuncMap`, so requests to plugin endpoints fall through to `executeDockerRequest` and are forwarded to the Docker daemon without any Portainer-side authorization check. ## Impact An authenticated, non-admin Portainer user with access to any Docker-enabled endpoint can: - Install and enable arbitrary Docker plugins from any registry. - Execute plugin code with root privileges on the Docker host (including declaring `CAP_SYS_ADMIN` and host-path mounts). - Read and modify files on the host filesystem from a restricted account, overriding the administrator's security policy. ## Timeline - 2026-03-16: Reported via GitHub Security Advisory by **ikkebr**. - 2026-04-20: Fix merged to `develop`, `release/2.39`, and `release/2.33`. - 2026-04-29: 2.41.0 released. - 2026-05-07: 2.39.2-LTS and 2.33.8-LTS released. ## Credit - **ikkebr** — identified and reported the proxy allowlist bypass affecting the Docker plugin management endpoints.
- risk 0.45cvss —epss —
## Summary Portainer's authentication middleware accepts JWT bearer tokens passed as the `?token=<JWT>` URL query parameter on any authenticated API endpoint, in addition to the standard `Authorization: Bearer` header. URLs are recorded in reverse-proxy access logs, browser history, and HTTP `Referer` headers on outbound navigation, so any JWT passed this way can be harvested by anyone with access to those logs or by an external site the user subsequently visits. A leaked token grants the full privileges of the user it was issued to, until the token expires (default 8 hours, configurable). The `?token=` parameter was used by Portainer's browser-based container attach, exec, and pod shell features, so any user with exec or attach rights on a container was exposed — not only administrators. ## Severity **High** Attack complexity is High because exploitation depends on the attacker obtaining a leaked token from a log, referer, or shared URL. Once obtained, a leaked token grants the privileges of the user it was issued to; for administrator tokens this compromises confidentiality, integrity, and availability of Portainer itself and of every Docker/Kubernetes environment it manages — container exec and stack deployment make host-level compromise reachable, so subsequent-system impact is also High. ## Affected Versions Query-parameter token acceptance has existed since JWT authentication was introduced in Portainer. Fixes are included in the following releases: | Branch | First vulnerable | Fixed in | |---------------------|------------------|------------| | 2.33.x (LTS) | 2.33.0 | **2.33.8** | | 2.39.x (LTS) | 2.39.0 | **2.39.2** | | 2.40.x (STS) | all prior | **2.41.0** | Portainer releases prior to 2.33.0 are end-of-life and will not receive a fix. Users on EOL versions should upgrade to a supported LTS branch. ## Workarounds Administrators who cannot immediately upgrade can reduce exposure by: - **Stripping `?token=` at the reverse proxy.** A rewrite rule in nginx, Traefik, or equivalent that removes the `token` query parameter before the request reaches Portainer blocks the query-parameter auth path entirely. Container exec and interactive shells rely on the query-parameter token for WebSocket upgrade and will stop working until the patched release is deployed. - **Auditing existing logs.** Search reverse-proxy access logs and application logs for `?token=` or `&token=` occurrences and treat any captured JWT as compromised. Resetting the affected user's password invalidates their sessions; reducing the JWT session timeout in Portainer settings shortens the exposure window for tokens already issued. - **Administrator hygiene.** Do not share Portainer URLs that contain `?token=` in chat, email, or tickets, and avoid navigating to external sites from within the Portainer UI on unpatched instances — the `Referer` header will carry the token. None of these replace the fix. ## Affected Code Pre-fix, `extractBearerToken` in `api/http/security/bouncer.go` read the JWT from the `token` query parameter before falling back to the `Authorization` header. The `query.Del("token")` call scrubs the parameter from `r.URL.RawQuery` on the way through Portainer, but by that point the original URL has already been recorded by any upstream reverse proxy, access logger, or browser. ```go func extractBearerToken(r *http.Request) (string, bool) { query := r.URL.Query() token := query.Get("token") if token != "" { query.Del("token") r.URL.RawQuery = query.Encode() return token, true } tokens, ok := r.Header[jwtTokenHeader] if !ok || len(tokens) == 0 { return "", false } // ... } ``` The fix removes the query-parameter path entirely. Authenticated requests now carry the JWT via the `Authorization` header for API clients, or via the `portainer_api_key` HttpOnly cookie for the browser UI — cookies are sent automatically on same-origin WebSocket upgrade requests, so the browser-based container attach, exec, and pod shell features continue to work without exposing the token in the URL. The WebSocket handlers that previously documented `?token=` as a required query parameter have been updated to match. ## Impact - **Token leakage to infrastructure.** Intermediate systems that observe the request URL — reverse proxies, load balancers, access logs, WAFs, and corporate network monitoring — capture the full JWT in plaintext. - **Token leakage via the browser.** URLs containing `?token=` are recorded in browser history and forwarded in the `Referer` header on any outbound navigation from the Portainer UI. - **Account takeover.** Anyone with access to a leaked JWT acts as the authenticated user for the remainder of the token's validity, without needing the password. If the leaked token belongs to an administrator, the attacker gains full API access including user management, container exec, and stack deployment. - **Reach beyond Portainer.** Container exec with an administrator JWT reaches the host filesystem of managed environments and can be used to execute commands on those hosts. ## Timeline - 2026-03-06: Reported via GitHub Security Advisory by **scanpwn**. - 2026-04-14: Fix merged to `develop`. - 2026-04-29: 2.41.0 released. - 2026-05-07: 2.39.2-LTS and 2.33.8-LTS released. ## Credit - **scanpwn** — identified and reported the query-parameter JWT acceptance and the resulting token-leakage vectors.
- risk 0.45cvss —epss —
## Summary Portainer proxies requests to Kubernetes clusters through a middleware layer (`kubeClientMiddleware`) that validates the requesting user's token before forwarding traffic to the cluster. When `security.RetrieveTokenData` returned an error, the middleware wrote an HTTP 403 response but was missing a `return` statement — execution continued into the handler with a nil `tokenData` value. The Kubernetes endpoints sit behind Portainer's outer `AuthenticatedAccess` bouncer, so an attacker requires a valid Portainer session. However, a user whose secondary token validation fails in `kubeClientMiddleware` — for example a user without permission to access a given Kubernetes endpoint — would have their request forwarded to the cluster anyway, bypassing the authorization check. The same defect was present in both the CE and EE codebases. ## Severity **High** **CWE-863** — Incorrect Authorization Privilege required is Low — any valid Portainer session is sufficient to reach the middleware. Once the authorization outcome is bypassed, the attacker can read and modify Kubernetes resources on the target endpoint that their role should not permit — confidentiality and integrity impact are both High. No availability impact is introduced directly. ## Affected Versions The missing `return` statement has been present since Kubernetes proxy support was introduced. | Branch | First vulnerable | Fixed in | |--------------|------------------|------------| | 2.33.x (LTS) | 2.33.0 | **2.33.8** | Portainer 2.39.0 and later are not affected — the fix was present from the initial 2.39.0 release. All releases prior to 2.33.0 are end-of-life and will not receive a fix; users on EOL versions should upgrade to a supported release. ## Workarounds There is no configuration change that prevents the bypass directly. Administrators who cannot immediately upgrade can reduce exposure by: - **Restricting Kubernetes endpoint access.** Remove Portainer access to Kubernetes endpoints for users who do not require it. A user without endpoint access cannot reach `kubeClientMiddleware`. - **Auditing Kubernetes RBAC.** Ensure the service account Portainer uses to proxy cluster requests carries least-privilege RBAC permissions — this limits the blast radius if the bypass is exploited. Neither of these replaces the fix. ## Affected Code `kubeClientMiddleware` in `api/http/handler/kubernetes/handler.go` wrote the error response but did not return, allowing execution to continue with nil `tokenData`: ```go // api/http/handler/kubernetes/handler.go (pre-fix — CE and EE) tokenData, err := security.RetrieveTokenData(r) if err != nil { httperror.WriteError(w, http.StatusForbidden, "permission denied to access the environment", err) // missing return — tokenData is nil, execution continues } // tokenData.ID dereferenced on the next line: _, ok := handler.KubernetesClientFactory.GetProxyKubeClient( strconv.Itoa(endpointID), strconv.Itoa(int(tokenData.ID))) ``` The fix adds a single return after the WriteError call in both CE and EE: ```go // post-fix if err != nil { httperror.WriteError(w, http.StatusForbidden, "permission denied to access the environment", err) return } ``` ## Impact - Kubernetes authorization bypass. A low-privileged Portainer user can reach Kubernetes API endpoints on environments their role does not permit, with the proxy client of the legitimate session used as the vehicle. - Cluster resource access. Depending on the service account permissions Portainer holds on the cluster, the attacker can read or modify namespaced resources — including pods, secrets, config maps, and deployments. - Potential for lateral movement. Kubernetes secrets readable through this path may contain credentials for other services within the cluster or the broader infrastructure. ## Timeline - 2026-02-16: Fix merged to develop. - 2026-02-25: 2.39.0 released with fix. - 2026-05-07: 2.33.8 released with backport fix.
- risk 0.45cvss —epss —
## Summary Portainer supports deploying stacks from Git repositories. When a Git-backed stack is created or updated, Portainer clones the repository using `go-git` v5, which translates Git blob entries with mode `0o120000` (symlink) into real OS symlinks on the host filesystem via `os.Symlink`. The only entry blocked from becoming a symlink is `.gitmodules`; every other path — including `docker-compose.yml`, which Portainer treats as the stack entry point — is created as a symlink without validation. Portainer's `GET /api/stacks/{id}/file` endpoint then reads the stack entry point with `os.ReadFile`, which follows OS symlinks transparently. A repository containing `docker-compose.yml` as a symlink to an arbitrary filesystem path (for example `/etc/passwd` or a mounted Kubernetes service account token) causes the symlink target's contents to be returned verbatim in the HTTP response. Any authenticated user with rights to create or update a Git-backed stack — the default configuration in Portainer CE — can read arbitrary files accessible to the Portainer process. The issue is amplified by Git-stack auto-update: an attacker can create a stack from a legitimate repository, pass initial review, and later push a commit that replaces `docker-compose.yml` with a symlink; the file read is then triggered on the next scheduled update cycle with no further interaction required. ## Severity **High** Attack complexity is Low: the attacker needs only the ability to host a Git repository and the default-granted permission to create a Git-backed stack. Privilege required is Low in typical CE deployments, where non-admin users can manage their own stacks; administrators retain the same attack surface regardless of the setting. Impact on confidentiality is High — the Portainer process commonly runs as root (required for Docker socket access), so arbitrary file read includes `/etc/shadow`, Kubernetes service account tokens, Docker secrets, environment variables, and the Portainer database itself. Integrity and availability are not directly affected, but the leaked contents (service account tokens, registry credentials, database session keys) frequently enable onward compromise of the host and managed environments. ## Affected Versions The vulnerability exists in every Portainer release since the introduction of Git-based stack deployment support — Git-backed stacks have always performed an unrestricted `go-git` checkout and subsequently read the entry-point file through `os.ReadFile` without resolving symlinks. Fixes are included in the following releases: | Branch | First vulnerable | Fixed in | |--------------|------------------|------------| | 2.33.x (LTS) | 2.33.0 | **2.33.8** | | 2.39.x (LTS) | 2.39.0 | **2.39.2** | | 2.40.x (STS) | all prior | **2.41.0** | Portainer releases prior to 2.33.0 are end-of-life and will not receive a fix. Users on EOL versions should upgrade to a supported LTS branch. ## Workarounds Administrators who cannot immediately upgrade can reduce exposure by: - **Restricting who can create Git-backed stacks.** Disable **Allow non-admin users to manage their stacks** in environment settings so that only administrators can submit a Git repository URL. This reduces the attack to an administrator-only surface but does not remove it. - **Avoiding untrusted repositories.** Do not deploy Git-backed stacks from repositories you do not control or review, and do not grant stack-management rights to users who can supply an arbitrary repository URL. - **Disabling auto-update on existing stacks.** Auto-update re-clones the repository on a schedule, which allows a repository that was safe at creation time to later become malicious. Disabling auto-update removes the deferred-exploitation path. - **Auditing existing stack working directories.** Search project paths under `/data/compose/` (or your configured data directory) for symlink entries — `find /data/compose -type l` — and treat any unexpected results as potential evidence of past exploitation. None of these replace the fix. ## Affected Code The vulnerability is the combination of two primitives. `go-git` translates Git symlink entries into OS symlinks unconditionally (except `.gitmodules`): ```go // go-git v5 — Worktree.checkoutFileSymlink func (w *Worktree) checkoutFileSymlink(f *object.File) (err error) { if strings.EqualFold(f.Name, gitmodulesFile) { return ErrGitModulesSymlink } // ... reads blob content as raw bytes ... err = w.Filesystem.Symlink(string(bytes), f.Name) return } ``` Relative symlink targets (`../../etc/passwd`) are passed through to `os.Symlink` as-is and escape the worktree at OS resolution time. (Absolute targets are chrooted to the worktree by `go-billy`'s `ChrootHelper.Symlink` and are not useful to the attacker.) On the read side, `GetFileContent` in `api/filesystem/filesystem.go` applies lexical path containment but not symlink resolution: ```go func (service *Service) GetFileContent(trustedRoot, filePath string) ([]byte, error) { content, err := os.ReadFile(JoinPaths(trustedRoot, filePath)) return content, err } ``` `JoinPaths` prevents `../` traversal in the input string but does not call `filepath.EvalSymlinks`, so a symlink already written to the project path resolves through `os.ReadFile` to its ultimate target. The fix wraps the `go-billy` filesystem used by the Git checkout with a custom `noSymlinkFS` type whose `Symlink()` method returns `ErrSymlinkDetected`, causing the clone to fail rather than write any OS symlink. Git trees that would otherwise produce a symlink entry are rejected at checkout time, closing the primary attack path. On the 2.33.x and 2.39.x branches the fix also hardens `GetFileContent` to call `filepath.EvalSymlinks` and verify the resolved path remains inside the trusted root, providing a second layer of defence against any future regression in Git-checkout handling. ## Impact - **Arbitrary file read as the Portainer process.** Any file readable by the Portainer process — typically root in containerized deployments — can be returned through the stack file endpoint. Common targets include `/etc/shadow`, `/root/.ssh/*`, `/proc/self/environ`, and the Portainer BoltDB (`portainer.db`) which contains all user password hashes, API tokens, and agent credentials. - **Kubernetes service account token exposure.** Portainer running on Kubernetes has its cluster service account token mounted at `/var/run/secrets/kubernetes.io/serviceaccount/token`; reading it grants the attacker the Portainer pod's cluster API access. - **Docker Swarm secret exposure.** Secrets mounted into the Portainer container at `/run/secrets/` (for example the initial admin password in Swarm deployments) are readable with the same mechanism. - **Onward compromise.** Leaked service tokens, registry credentials, and database contents frequently enable authenticated access to managed Docker/Kubernetes environments, container registries, and Portainer itself under other users' identities. - **Deferred exploitation via auto-update.** A repository that passes initial review at stack creation can be mutated afterwards; the malicious commit takes effect on the next auto-update cycle without user interaction. ## Timeline - 2026-03-20: Reported via GitHub Security Advisory by **b-hermes**. - 2026-04-18: Fix merged to `develop`. - 2026-04-29: 2.41.0 released with fix. - 2026-05-07: 2.33.8, 2.39.2, released with fix. ## Credit - **b-hermes** — identified the Git symlink injection primitive, traced the end-to-end chain through `GetFileContent`, and provided a fully validated proof-of-concept.
- risk 0.45cvss —epss —
## Summary Portainer offers an environment-level **Disable bind mounts for non-administrators** security setting that blocks regular users from binding host paths into containers they create through the Portainer-mediated Docker API. The check that enforces this setting only inspected the legacy `HostConfig.Binds` array on the container-create proxy and never looked at the equivalent `HostConfig.Mounts` array. Any authenticated user with rights to create containers on a Docker environment where the restriction is enabled could submit a `bind`-typed entry under `HostConfig.Mounts` and mount any host path into their container. The two fields are interchangeable on the Docker daemon — both produce real bind mounts at runtime — so a check that inspects only one is functionally equivalent to no check at all. The same primitive is correctly enforced on Swarm service create against `TaskTemplate.ContainerSpec.Mounts`; the gap was specific to the `POST /containers/create` proxy path. Exploitation requires a regular user with container-create rights on an environment that has the restriction enabled. Such a user can mount any host path read-write or read-only into a container they own and use the resulting view of the host filesystem to read or write anything the Docker daemon's user can — typically `root`. Bind-mount restriction is the primary defence against host filesystem exposure on shared environments where regular users are otherwise permitted to deploy containers. ## Severity **High** The vulnerability is exploitable over the network with low attack complexity, no attack requirement, and no user interaction. It requires a low-privilege authenticated session — any regular user with container-create rights on an environment where the bind-mount restriction is enabled. The vulnerable system (the Portainer container-create proxy) suffers a confidentiality and integrity breach by virtue of the bypass itself, but the dominant impact is on the subsequent system: the Docker host's filesystem and any container running alongside the attacker's. This is a restriction bypass rather than a cross-authority escalation — the user already had container-create rights, and the bind-mount restriction is a defence-in-depth control on top of that capability — which is the reason the rating is held at High rather than promoted to Critical despite the host-level reach. ## Affected Versions The vulnerability has existed since the `AllowBindMountsForRegularUsers` security setting was introduced. The `HostConfig.Mounts` field has never been inspected by the container-create proxy on any release line. Fixes are included in the following releases: | Branch | First vulnerable | Fixed in | |--------------|------------------|------------| | 2.33.x (LTS) | 2.33.0 | **2.33.8** | | 2.39.x (LTS) | 2.39.0 | **2.39.2** | | 2.41.x (STS) | all prior | **2.41.0** | Portainer releases prior to 2.33.0 are end-of-life and will not receive a fix. Users on EOL versions should upgrade to a supported LTS branch. ## Workarounds Administrators who cannot immediately upgrade can reduce exposure by: - **Revoke container-create rights from non-administrator accounts on affected environments.** If the bind-mount restriction is being relied on as a hard guarantee, audit which non-administrator accounts have container-create rights on environments where it is set, and downgrade those accounts to roles that lack container-create until the patched release is deployed. Stack and service deployment that depends on container-create will stop working for those users until the patched release is in place. - **Audit recent container creations for `HostConfig.Mounts` of `Type: bind` from non-admin Portainer users.** Inspect Docker daemon logs and `docker inspect` output on affected environments. Any non-admin-created container with a bind-typed `Mounts` entry should be treated as a potential incident. - **Segregate tenants by environment.** Where the per-environment toggle was being used to share an environment between tenants of different trust levels, splitting the workloads onto separate environments is a stronger control than the toggle and remains in place after upgrade. None of these replace the fix. ## Affected Code The enforcement lives in `decorateContainerCreationOperation` in `package/server-ce/api/http/proxy/factory/docker/containers.go`. The `PartialContainer` struct used to deserialise the request body for inspection only contained `HostConfig.Binds`: ``` // package/server-ce/api/http/proxy/factory/docker/containers.go type PartialContainer struct { HostConfig struct { Privileged bool `json:"Privileged"` PidMode string `json:"PidMode"` Devices []any `json:"Devices"` Sysctls map[string]any `json:"Sysctls"` CapAdd []string `json:"CapAdd"` CapDrop []string `json:"CapDrop"` Binds []string `json:"Binds"` } `json:"HostConfig"` } if !securitySettings.AllowBindMountsForRegularUsers && len(partialContainer.HostConfig.Binds) > 0 { for _, bind := range partialContainer.HostConfig.Binds { if strings.HasPrefix(bind, "/") { return forbiddenResponse, ErrBindMountsForbidden } } } ``` The fix adds a `Mounts` field to `PartialContainer` and a parallel check that rejects any entry whose `Type` equals `bind`, mirroring the existing logic on the Swarm service-create proxy. The container-update path is unaffected — the Docker daemon does not accept mount changes via container update — and Swarm service create was already covered. Compose-stack deployment is not in scope of this advisory; the bind-mount restriction is a daemon-mediated container-create control, and Compose deployment runs Docker through a separate path that is not currently subject to the same restriction. The same change applies cleanly on each LTS branch — the surrounding code shape on `release/2.33` and `release/2.39` is identical to develop on the points the patch touches, so the LTS backports are byte-equivalent additions of the `Mounts` field and the parallel check. ## Impact A regular user who has been explicitly restricted from using bind mounts can bypass the restriction and: - **Read or write any path on the Docker host filesystem.** The mount runs as the daemon user (typically `root`), so any path is reachable. Sensitive examples include `/etc/shadow`, host SSH keys under `/root/.ssh` and `/home/*/.ssh`, and TLS material under `/etc/docker`. - **Compromise other containers on the same host.** The host's `/var/lib/docker` (or equivalent) is reachable from within the bound mount, exposing the layers, volumes, and live state of every container the daemon manages. - **Reach the Docker socket.** Mounting `/var/run/docker.sock` into the attacker's container hands them full Docker API access on the host, regardless of any authorisation enforced by Portainer above the proxy. - **Write persistence to the host.** Without `ReadOnly`, the attacker can drop SSH keys into `authorized_keys`, install systemd units, or modify cron, achieving persistence outside of any container the daemon supervises. The bind-mount restriction was the primary defence against this class of host exposure for non-administrator container creators; bypassing it removes the only enforcement point above the daemon for tenants who were granted container-create rights. ## Timeline - 2026-03-04: Reported via GitHub Security Advisory by **offensiveee** (Assaf Alassaf). - 2026-03-04 – 2026-04-17: Six further independent reports of the same primitive received via GitHub Security Advisory (alexwaira, ffulbtech, Proscan-one, jeroengui, AyushParkara, marduc812) and consolidated against this advisory. - 2026-04-18: Fix merged to `develop`. - 2026-06-04: Backports merged to `release/2.33` and `release/2.39`. - 2026-04-29: 2.41.0 released with fix. - 2026-05-07: 2.33.8, 2.39.2, released with fix. ## Credit - **offensiveee** (Assaf Alassaf) — initial report identifying the `HostConfig.Mounts` bypass on container create and the divergence from the Swarm service-create check. - **alexwaira** — independently reported the same `HostConfig.Mounts` bypass on container create. - **ffulbtech** — independently reported the same `HostConfig.Mounts` bypass on container create. - **Proscan-one** — independently reported the same `HostConfig.Mounts` bypass on container create. - **jeroengui** — independently reported the same `HostConfig.Mounts` bypass on container create. - **AyushParkara** — independently reported the same `HostConfig.Mounts` bypass on container create. - **marduc812** — independently reported the same `HostConfig.Mounts` bypass on container create.
- risk 0.37cvss 6.8epss 0.00
Portainer Community Edition is a lightweight service delivery platform for containerized applications that can be used to manage Docker, Swarm, Kubernetes and ACI environments. Prior to STS version 2.31.0 and LTS version 2.27.7, if a Portainer administrator can be convinced to register a malicious container registry, or an existing container registry can be taken over, HTTP Headers (including registry authentication credentials or Portainer session tokens) may be leaked to that registry. This issue has been patched in STS version 2.31.0 and LTS version 2.27.7.
- CVE-2026-44884May 14, 2026risk 0.00cvss —epss —
## Summary A missing authorization vulnerability in the Custom Template file endpoint (`GET /api/custom_templates/{id}/file`) allows any authenticated user to read the file content of any custom template by enumerating sequential integer IDs, bypassing Resource Control access restrictions. Template files may contain environment-specific values such as connection strings, API tokens, or registry credentials that administrators would not expect standard users to read. ## Severity **Medium** **CWE-862** — Missing Authorization Exploitation requires an authenticated user account and at least one custom template to exist. Template files are returned verbatim and may contain embedded credentials. ## Affected Versions The vulnerability exists in every Portainer release since custom templates were introduced — the `customTemplateFile` handler has never performed an authorization check. Fixes are included in the following releases: | Branch | First vulnerable | Fixed in | |---------------------|------------------|------------| | 2.33.x (LTS) | 2.33.0 | **2.33.8** | | 2.39.x (LTS) | 2.39.0 | **2.39.1** | Portainer 2.40.0 and later are not affected — the fix was already on `develop` when the 2.40.x STS line branched. Portainer LTS branches receive fixes for 6 months plus a 3-month overlap after the next LTS ships. All releases **prior to 2.33.0 are end-of-life** and will not receive a fix; users on EOL versions should upgrade to a supported release. ## Workarounds There is no runtime configuration that blocks the vulnerable endpoint directly. Administrators who cannot immediately upgrade can reduce exposure by: - **Avoiding storing secrets in custom templates** until the patched release is deployed. Move sensitive configuration values to Portainer environment variables or an external secret store. - **Reviewing existing custom templates** for embedded secrets. Assume any secret previously stored in a custom template on an unpatched instance has been exposed to every authenticated user and rotate accordingly. Neither replaces the fix. ## Affected Code The `customTemplateFile` handler in `api/http/handler/customtemplates/customtemplate_file.go` (lines 30-53) retrieves a custom template by its numeric ID and returns the file content without performing any authorization check. All other custom template endpoints properly verify access: | Endpoint | Method | Authorization Check | |----------|--------|-------------------| | `/api/custom_templates/{id}` | GET (inspect) | `userCanEditTemplate()` + `UserCanAccessResource()` | | `/api/custom_templates/{id}` | PUT (update) | `userCanEditTemplate()` | | `/api/custom_templates/{id}` | DELETE | `userCanEditTemplate()` | | `/api/custom_templates` | GET (list) | `FilterAuthorizedCustomTemplates()` | | **`/api/custom_templates/{id}/file`** | **GET** | **None** | **Vulnerable code** (`customtemplate_file.go:30-53`): ```go func (handler *Handler) customTemplateFile(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { customTemplateID, _ := request.RetrieveNumericRouteVariableValue(r, "id") customTemplate, _ := handler.DataStore.CustomTemplate().Read(portainer.CustomTemplateID(customTemplateID)) // NO AUTHORIZATION CHECK fileContent, _ := handler.FileService.GetFileContent(customTemplate.ProjectPath, entryPath) return response.JSON(w, &fileResponse{FileContent: string(fileContent)}) } ``` **Secure reference** (`customtemplate_inspect.go:50-75`): ```go canEdit := userCanEditTemplate(customTemplate, securityContext) hasAccess := authorization.UserCanAccessResource(securityContext.UserID, teamIDs, resourceControl) if !canEdit && !hasAccess { return httperror.Forbidden("Access denied to resource", httperrors.ErrResourceAccessDenied) } ``` ## Impact Any authenticated user (including the lowest-privilege standard user) can read the file content of every custom template in the instance. Custom templates commonly contain Docker Compose configuration, which may include environment-specific secrets such as database connection strings, API tokens, or registry credentials. ## Timeline - 2026-02-11: Reported via GitHub Security Advisory by **duddnr0615k**. - 2026-03-04: Fix merged to `develop` and cherry-picked to `release/2.39`. - 2026-03-19: 2.39.1 released with fix. - 2026-03-25: 2.40.0 released with fix already present from branch cut. - 2026-05-07: 2.33.8 released. ## Credit - **duddnr0615k** — identified and reported the missing authorization check on the custom template file endpoint.
- CVE-2026-44885May 14, 2026risk 0.00cvss —epss —
### Summary Portainer's backup restore feature accepts a `.tar.gz` archive and extracts it to a target directory on the server. The extraction function (`ExtractTarGz` in `api/archive/targz.go`) constructed output paths using `filepath.Clean(filepath.Join(outputDirPath, header.Name))`. This combination does not prevent directory traversal — a tar entry named `../../etc/cron.d/evil` resolves to a path outside the extraction root, so a crafted archive can write files to arbitrary locations on the server filesystem. ## Severity **Medium** **CWE-22** — Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') Exploitation requires administrator access to Portainer's backup restore endpoint. An administrator who is deceived into restoring a malicious archive, or whose credentials are compromised, can use this path to write files outside the Portainer data directory. ## Affected Versions The vulnerability exists in every Portainer release prior to 2.39.0 — `ExtractTarGz` has used `filepath.Clean(filepath.Join())` since it was introduced. The fix shipped with 2.39.0 (patched on `develop` before the 2.39 branch cut); 2.34.x–2.38.x STS releases are also affected but are end-of-life and will not receive a fix. | Branch | First vulnerable | Fixed in | |--------------|------------------|------------| | 2.33.x (LTS) | 2.33.0 | **2.33.8** | Portainer 2.39.0 and later are not affected — the fix was present from the initial 2.39.0 release. All releases prior to 2.33.0 are end-of-life and will not receive a fix; users on EOL versions should upgrade to a supported release. ## Workarounds Administrators who cannot immediately upgrade should: - **Only restore archives from trusted sources.** Do not restore archives received from untrusted parties or transmitted over unencrypted channels. - **Use backup encryption.** Portainer's optional backup encryption requires the correct passphrase to decrypt before extraction; an attacker without the passphrase cannot craft a valid encrypted archive. Neither of these replaces the fix. ## Affected Code `ExtractTarGz` in `api/archive/targz.go` constructed output paths without safe containment: ```go // api/archive/targz.go (pre-fix) case tar.TypeReg: p := filepath.Clean(filepath.Join(outputDirPath, header.Name)) ``` filepath.Join resolves ../ components lexically and filepath.Clean normalises the result, but neither verifies the final path remains inside outputDirPath. The fix replaces this with filesystem.JoinPaths, which forces all path components to be relative to the trusted root: ```go // api/archive/targz.go (post-fix) case tar.TypeReg: p := filesystem.JoinPaths(outputDirPath, header.Name) ``` ### Impact - Arbitrary file write at any path accessible to the Portainer process (typically root in containerised deployments), overriding filesystem boundaries of the data directory. - Potential host persistence by writing to cron directories, SSH authorised key files, or executable paths, depending on how the container is configured and what host paths are accessible. The practical severity is reduced because exploitation requires administrative privileges within Portainer. ## Timeline - 2026-02-16: Fix merged to develop ([#1875](https://github.com/portainer/portainer-suite/pull/1875)). - 2026-02-25: 2.39.0 released with fix. - 2026-05-07: 2.33.8 released with backport fix. ### Credits Reported by [Kolega](https://kolega.ai).