API: bound jq evaluation on /api/state against unauthenticated DoS (#33028)

This commit is contained in:
Tilo Alexander 2026-08-21 09:24:41 +02:00 • committed by GitHub
parent 20058f6e0d
commit 769a6a1aa5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 120 additions and 4 deletions

View file

@ -1,6 +1,7 @@
package server
import (
"bytes"
"context"
"encoding/json"
"errors"
@ -30,6 +31,13 @@ import (
var ignoreState = []string{"releaseNotes"} // excessive size
// limits for the unauthenticated jq parameter of the state endpoint
const (
maxJqQueryLen = 512 // maximum length of the jq query
maxJqDuration = time.Second // maximum jq evaluation time
maxJqResultBytes = 1 << 20 // maximum size of the encoded jq result
)
// getPreferredLanguage returns the preferred language as two letter code
func getPreferredLanguage(header string) string {
languages, _, err := language.ParseAcceptLanguage(header)
@ -117,6 +125,24 @@ func jsonWrite(w http.ResponseWriter, data any) {
json.NewEncoder(w).Encode(data)
}
// jsonWriteLimited writes data as json, failing if the encoded result exceeds limit bytes.
// Encoding into a buffer keeps oversized results from reaching the client at all.
func jsonWriteLimited(w http.ResponseWriter, data any, limit int) {
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(data); err != nil {
jsonError(w, http.StatusBadRequest, err)
return
}
if buf.Len() > limit {
jsonError(w, http.StatusBadRequest, errors.New("result too large"))
return
}
w.Header().Set("Content-Type", "application/json")
buf.WriteTo(w)
}
func jsonError(w http.ResponseWriter, status int, err error) {
w.WriteHeader(status)
jsonWrite(w, util.ErrorAsJson(err))
@ -256,6 +282,11 @@ func stateHandler(cache *util.ParamCache) http.HandlerFunc {
if q := r.URL.Query().Get("jq"); q != "" {
q = strings.TrimPrefix(q, ".result")
if len(q) > maxJqQueryLen {
jsonError(w, http.StatusBadRequest, errors.New("jq: query too long"))
return
}
query, err := gojq.Parse(q)
if err != nil {
jsonError(w, http.StatusBadRequest, err)
@ -268,13 +299,21 @@ func stateHandler(cache *util.ParamCache) http.HandlerFunc {
return
}
res, err := jq.Query(query, b)
// the query is attacker-controlled, so bound evaluation time and result size
ctx, cancel := context.WithTimeout(r.Context(), maxJqDuration)
defer cancel()
res, err := jq.QueryContext(ctx, query, b)
if err != nil {
jsonError(w, http.StatusBadRequest, err)
status := http.StatusBadRequest
if ctx.Err() != nil {
status = http.StatusServiceUnavailable
}
jsonError(w, status, err)
return
}
jsonWrite(w, res)
jsonWriteLimited(w, res, maxJqResultBytes)
return
}