Technical detail
For readers who want the mechanics. Everything here comes from Portainer’s published advisories.
Bind-mount restriction bypass
- Severity
- Critical, CVSS 4.0 score 9.4
- Vector
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- Weakness
- CWE-284, improper access control
- Fixed in
- 2.45.2. The 2.39.x line had no fix at publication.
Docker’s local volume driver turns the options type=none, o=bind, device=<host path> into a real bind mount of that host path. Portainer’s filters only blocked the literal forms. Container creation rejected mounts typed "bind" but never read a volume mount’s driver options. The volume and Swarm service filters compared type with "bind", while a bind mount keys off o. Every check was also case-sensitive, and Docker is not, so "Bind" passed. A separate path, automatic Git redeploy by webhook or polling, deployed the pulled Compose file without any security-setting check.
The container-creation filter, as quoted in the advisory:
// api/http/proxy/factory/docker/containers.go
if !securitySettings.AllowBindMountsForRegularUsers && len(partialContainer.HostConfig.Mounts) > 0 {
for _, mount := range partialContainer.HostConfig.Mounts {
if mount.Type == "bind" {
return forbiddenResponse, ErrBindMountsForbidden
}
}
}
The advisory credits our report with the container-creation bypass: the guard is a denylist that matches only the literal "bind", while the canonical local-driver form is type=none, o=bind, device=<host path>, and the container-creation request model never parses Mounts[].VolumeOptions at all.
The fix routes every mount-creating path through one shared helper. It treats a mount as a host bind when its type is bind or npipe in any letter case, when the o option contains bind or rbind, or when a device is set on the local driver. It also resolves named volumes, walks the Compose top-level volumes: block, and validates automatic Git redeploys.
Server-side request forgery
- Severity
- Medium, CVSS 4.0 score 5.3
- Vector
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:L/VI:N/VA:N/SC:L/SI:N/SA:N- Weakness
- CWE-918, server-side request forgery
- Fixed in
- 2.45.1, the first release that covers every path the advisory names
Git clones, Helm repositories, registry pings, template and manifest URLs, the GitLab proxy and the OAuth resource URI all took a destination from input and opened a connection to it. The shared validator checked only the URL’s syntax, as quoted in the advisory:
// pkg/validate/validate.go
func IsURL(urlString string) bool {
strTemp := urlString
if !strings.Contains(urlString, "://") {
strTemp = "http://" + urlString
}
u, err := url.Parse(strTemp)
return err == nil && u.Host != ""
}
Our report covered the Git repository file preview, where the upstream response was reflected in the clone error, so the request was not blind.
The fix adds a safe dialer. It resolves the hostname, checks every resolved address against the destination policy, then connects to the address it checked, which closes the DNS rebinding window. Redirects go back through the same dialer. The policy has an Audit and an Enforce mode, ships disabled, and can only be switched on in Business Edition.