CVE-2019-13209
Description
Rancher 2 through 2.2.4 is vulnerable to Cross-Site Websocket Hijacking, allowing an attacker to execute Kubernetes API commands with the victim's identity.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Rancher 2 through 2.2.4 is vulnerable to Cross-Site Websocket Hijacking, allowing an attacker to execute Kubernetes API commands with the victim's identity.
Overview
CVE-2019-13209 is a Cross-Site Websocket Hijacking vulnerability in Rancher, a Kubernetes management platform, affecting versions 2.0 to 2.2.4. The root cause is the lack of a cross-origin check for WebSocket connections, as confirmed by the security patch that adds an origin check in the websocket handler [1][3].
Exploitation
For exploitation, the victim must be authenticated to a Rancher server and visit a malicious third-party website while logged in. Under these conditions, the attacker can trigger a WebSocket connection to the Rancher server from the attacker-controlled site, without proper origin validation. By doing so, the attacker can hijack a management channel and issue arbitrary Kubernetes API requests using the victim's credentials and permissions [1][2].
Impact
Successful exploitation allows the attacker to execute commands on the targeted Kubernetes cluster with the full privileges of the victim user. This can lead to unauthorized access to cluster resources, configuration changes, data exfiltration, or further compromise of managed workloads [2].
Mitigation
The vulnerability is addressed in Rancher release v2.2.5, which introduces an origin check for WebSocket requests [1][3]. Users should upgrade to v2.2.5 or later. No workarounds are mentioned in the public sources; upgrading is the recommended mitigation.
AI Insight generated on May 22, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/rancher/rancherGo | >= 2.0.0, < 2.0.16 | 2.0.16 |
github.com/rancher/rancherGo | >= 2.1.0, < 2.1.11 | 2.1.11 |
github.com/rancher/rancherGo | >= 2.2.0, < 2.2.5 | 2.2.5 |
Affected products
2- Rancher/Rancherdescription
Patches
10ddffe484adcAdd websocket origin check
4 files changed · +118 −3
pkg/clusterrouter/router.go+6 −2 modified@@ -11,7 +11,6 @@ import ( ) type Router struct { - clusterLookup ClusterLookup serverFactory *factory } @@ -25,7 +24,12 @@ func New(localConfig *rest.Config, lookup ClusterLookup, dialer dialer.Factory, func (r *Router) ServeHTTP(rw http.ResponseWriter, req *http.Request) { c, handler, err := r.serverFactory.get(req) if err != nil { - response(rw, httperror.ServerError, err.Error()) + e, ok := err.(*httperror.APIError) + if ok { + response(rw, e.Code, e.Message) + } else { + response(rw, httperror.ServerError, err.Error()) + } return }
pkg/websocket/handler.go+89 −0 added@@ -0,0 +1,89 @@ +package websocket + +import ( + "encoding/json" + "net/http" + "net/url" + "strings" + + "github.com/rancher/norman/httperror" +) + +func NewWebsocketHandler(handler http.Handler) http.Handler { + return &websocketHandler{ + handler, + } +} + +type websocketHandler struct { + next http.Handler +} + +func (h websocketHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + if isWebsocket(req) { + if !checkSameOrigin(req) { + response(rw, httperror.PermissionDenied, "origin not allowed") + return + } + } + h.next.ServeHTTP(rw, req) +} + +// Inspired by https://github.com/gorilla/websocket/blob/80c2d40e9b91f2ef7a9c1a403aeec64d1b89a9a6/server.go#L87 +// checkSameOrigin returns true if the origin is not set or is equal to the request host. +func checkSameOrigin(r *http.Request) bool { + origin := r.Header["Origin"] + if len(origin) == 0 { + return true + } + u, err := url.Parse(origin[0]) + if err != nil { + return false + } + + if u.Port() == "" { + return u.Host == r.Host + } + return u.Host == r.Host && u.Port() == portOnly(r.Host) +} + +// isWebsocket returns true if the request is a websocket +func isWebsocket(r *http.Request) bool { + if !headerListContainsValue(r.Header, "Connection", "upgrade") { + return false + } + return true +} + +// headerListContainsValue returns true if the token header with the given name contains token. +func headerListContainsValue(header http.Header, name string, value string) bool { + for _, v := range header[name] { + for _, s := range strings.Split(v, ",") { + if strings.EqualFold(value, strings.TrimSpace(s)) { + return true + } + } + } + return false +} + +func response(rw http.ResponseWriter, code httperror.ErrorCode, message string) { + rw.WriteHeader(code.Status) + rw.Header().Set("content-type", "application/json") + json.NewEncoder(rw).Encode(httperror.NewAPIError(code, message)) +} + +// portOnly returns the port part of localhost:port, without the leading colon +func portOnly(hostport string) string { + colon := strings.IndexByte(hostport, ':') + if colon == -1 { + return "" + } + if i := strings.Index(hostport, "]:"); i != -1 { + return hostport[i+len("]:"):] + } + if strings.Contains(hostport, "]") { + return "" + } + return hostport[colon+len(":"):] +}
server/server.go+3 −1 modified@@ -21,6 +21,7 @@ import ( "github.com/rancher/rancher/pkg/pipeline/hooks" "github.com/rancher/rancher/pkg/rkenodeconfigserver" "github.com/rancher/rancher/pkg/telemetry" + "github.com/rancher/rancher/pkg/websocket" "github.com/rancher/rancher/server/capabilities" "github.com/rancher/rancher/server/responsewriter" "github.com/rancher/rancher/server/ui" @@ -58,8 +59,9 @@ func Start(ctx context.Context, httpPort, httpsPort int, localClusterEnabled boo if err != nil { return err } + websocketHandler := websocket.NewWebsocketHandler(authedHandler) - auditHandler := audit.NewAuditLogFilter(ctx, auditLogWriter, authedHandler) + auditHandler := audit.NewAuditLogFilter(ctx, auditLogWriter, websocketHandler) webhookHandler := hooks.New(scaledContext)
tests/integration/suite/test_tokens.py+20 −0 modified@@ -1,3 +1,8 @@ +import pytest +import rancher +from .conftest import BASE_URL + + def test_certificates(admin_mc): client = admin_mc.client @@ -10,3 +15,18 @@ def test_certificates(admin_mc): currentCount += 1 assert currentCount == 1 + + +def test_websocket(admin_mc): + client = rancher.Client(url=BASE_URL, token=admin_mc.client.token, + verify=False) + # make a request that looks like a websocket + client._session.headers["Connection"] = "upgrade" + client._session.headers["Upgrade"] = "websocket" + client._session.headers["Origin"] = "badStuff" + # do something with client now that we have a "websocket" + + with pytest.raises(rancher.ApiError) as e: + client.list_cluster() + + assert e.value.error.Code.Status == 403
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-xhg2-rvm8-w2jhghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2019-13209ghsaADVISORY
- forums.rancher.com/c/announcementsmitrex_refsource_MISC
- forums.rancher.com/t/rancher-release-v2-2-5-addresses-rancher-cve-2019-13209/14801ghsax_refsource_CONFIRMWEB
- github.com/rancher/rancher/commit/0ddffe484adccb9e37d9432e8e625d8ebbfb0088ghsaWEB
News mentions
0No linked articles in our index yet.