API: bound jq evaluation on /api/state against unauthenticated DoS (#33028)
This commit is contained in:
parent
20058f6e0d
commit
769a6a1aa5
3 changed files with 120 additions and 4 deletions
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue