DoS / Memory exhaustion via unbounded HTTP header length in data proxy path

HIGH
grafana/grafana
Commit: 83838c58c15e
Affected: <=12.3.x (pre-12.4.0 releases where DataProxyForwardUserAgent could forward unbounded client UA)
2026-05-13 09:34 UTC

Description

The commit fixes a DoS/memory-safety risk in Grafana's Data Source plugin proxy by capping the forwarded client User-Agent when forward_user_agent is enabled. Prior to this change, an attacker could supply a very long or unbounded User-Agent header, which would be appended to the proxy UA and forwarded downstream, potentially causing resource exhaustion or memory issues in the data proxy path. The patch introduces a 255-byte cap for the original client User-Agent and truncates any excess before forwarding. Tests were added to verify the truncation behavior.

Proof of Concept

Proof-of-concept (pre-fix exploitation): Prerequisites: - Grafana 12.x with DataProxyForwardUserAgent enabled - A data source proxy route (e.g., /api/datasources/proxy/{id}/render) Attack vector: An attacker sends a request to the data-source proxy with an extremely long Client User-Agent header. The header is forwarded (and concatenated with the proxy UA) to the data source backend, potentially triggering excessive memory usage or DoS in the proxy path. Before fix (vulnerable behavior): the original User-Agent is appended in full, producing a very large combined header. After fix (patched behavior): the original UA is truncated to 255 bytes before being appended, limiting header size and reducing risk. Example exploit commands (adjust host/paths to your environment): # 1) Generate a very long client UA (1 MB) LONG=$(python3 -c 'print("A"*1000000)') # 2) Send a request to the data-source proxy with the long UA curl -i -H "User-Agent: GrafanaTest ${LONG}" http://grafana.local/api/datasources/proxy/1/render Expected outcome (pre-fix): the proxy forwards an extremely long User-Agent to downstream services, risking memory/resource exhaustion. Expected outcome (post-fix): the forwarded client UA is truncated to 255 bytes, mitigating the DoS risk.

Commit Details

Author: Marc Sanmiquel

Date: 2026-05-13 08:17 UTC

Message:

Data Source: Cap forwarded User-Agent length (#124704)

Triage Assessment

Vulnerability Type: DoS / Memory safety

Confidence: HIGH

Reasoning:

The commit introduces a maximum length cap for the forwarded User-Agent and truncates any client User-Agent exceeding 255 bytes when forward_user_agent is enabled. This mitigates potential issues from extremely long headers (e.g., resource exhaustion or memory handling problems) in the data proxy path. Tests have been added to verify correct truncation behavior.

Verification Assessment

Vulnerability Type: DoS / Memory exhaustion via unbounded HTTP header length in data proxy path

Confidence: HIGH

Affected Versions: <=12.3.x (pre-12.4.0 releases where DataProxyForwardUserAgent could forward unbounded client UA)

Code Diff

diff --git a/pkg/api/pluginproxy/ds_proxy.go b/pkg/api/pluginproxy/ds_proxy.go index 224125a773df6..56982f00b912f 100644 --- a/pkg/api/pluginproxy/ds_proxy.go +++ b/pkg/api/pluginproxy/ds_proxy.go @@ -36,6 +36,10 @@ var ( errPluginProxyRouteAccessDenied = errors.New("plugin proxy route access denied") ) +// maxForwardedUserAgentLen is the maximum byte length of the client User-Agent +// appended to the data proxy User-Agent when forward_user_agent is enabled. +const maxForwardedUserAgentLen = 255 + type DataSourceProxy struct { ds *datasources.DataSource ctx *contextmodel.ReqContext @@ -239,6 +243,9 @@ func (proxy *DataSourceProxy) director(req *http.Request) { ua := proxy.cfg.DataProxyUserAgent if proxy.cfg.DataProxyForwardUserAgent { if originalUA := req.Header.Get("User-Agent"); originalUA != "" { + if len(originalUA) > maxForwardedUserAgentLen { + originalUA = originalUA[:maxForwardedUserAgentLen] + } if ua != "" { ua = ua + " " + originalUA } else { diff --git a/pkg/api/pluginproxy/ds_proxy_test.go b/pkg/api/pluginproxy/ds_proxy_test.go index 0d276a20e1124..6034ccb51055a 100644 --- a/pkg/api/pluginproxy/ds_proxy_test.go +++ b/pkg/api/pluginproxy/ds_proxy_test.go @@ -866,6 +866,49 @@ func TestDataSourceProxy_userAgentHeader(t *testing.T) { assert.Equal(t, "original-client/1.0", req.Header.Get("User-Agent")) }) + + t.Run("When DataProxyForwardUserAgent is enabled and the client User-Agent exceeds the length cap, it is truncated", func(t *testing.T) { + ctx := &contextmodel.ReqContext{} + proxy, err := setupDSProxyTest(t, ctx, ds, routes, "/render", func(p *DataSourceProxy) { + p.cfg = &setting.Cfg{ + BuildVersion: "5.3.0", + DataProxyUserAgent: "Grafana/5.3.0", + DataProxyForwardUserAgent: true, + } + }) + require.NoError(t, err) + + oversized := strings.Repeat("a", maxForwardedUserAgentLen+100) + req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil) + require.NoError(t, err) + req.Header.Set("User-Agent", oversized) + + proxy.director(req) + + expected := "Grafana/5.3.0 " + strings.Repeat("a", maxForwardedUserAgentLen) + assert.Equal(t, expected, req.Header.Get("User-Agent")) + }) + + t.Run("When DataProxyForwardUserAgent is enabled and the client User-Agent is exactly the cap, it is forwarded unchanged", func(t *testing.T) { + ctx := &contextmodel.ReqContext{} + proxy, err := setupDSProxyTest(t, ctx, ds, routes, "/render", func(p *DataSourceProxy) { + p.cfg = &setting.Cfg{ + BuildVersion: "5.3.0", + DataProxyUserAgent: "Grafana/5.3.0", + DataProxyForwardUserAgent: true, + } + }) + require.NoError(t, err) + + exact := strings.Repeat("b", maxForwardedUserAgentLen) + req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil) + require.NoError(t, err) + req.Header.Set("User-Agent", exact) + + proxy.director(req) + + assert.Equal(t, "Grafana/5.3.0 "+exact, req.Header.Get("User-Agent")) + }) } // test DataSourceProxy request handling.
← Back to Alerts View on GitHub →