DoS / Memory exhaustion via unbounded HTTP header length in data proxy path
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.