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 jq
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
|
@ -10,12 +11,19 @@ import (
|
|||
|
||||
// Query executes a compiled jq query against given json. It expects a single result only.
|
||||
func Query(query *gojq.Query, input []byte) (any, error) {
|
||||
return QueryContext(context.Background(), query, input)
|
||||
}
|
||||
|
||||
// QueryContext executes a compiled jq query against given json, aborting when ctx is done.
|
||||
// It expects a single result only. Use this instead of Query whenever the query originates
|
||||
// from an untrusted source, since jq is turing-complete and evaluation may not terminate.
|
||||
func QueryContext(ctx context.Context, query *gojq.Query, input []byte) (any, error) {
|
||||
var j any
|
||||
if err := json.Unmarshal(input, &j); err != nil {
|
||||
return j, err
|
||||
}
|
||||
|
||||
iter := query.Run(j)
|
||||
iter := query.RunWithContext(ctx, j)
|
||||
|
||||
v, ok := iter.Next()
|
||||
if !ok {
|
||||
|
|
@ -23,6 +31,16 @@ func Query(query *gojq.Query, input []byte) (any, error) {
|
|||
}
|
||||
|
||||
if err, ok := v.(error); ok {
|
||||
// halt/halt_error do not terminate the iterator by themselves
|
||||
var he *gojq.HaltError
|
||||
if errors.As(err, &he) {
|
||||
return nil, errors.New("jq: query halted")
|
||||
}
|
||||
|
||||
if ctxErr := ctx.Err(); ctxErr != nil {
|
||||
return nil, fmt.Errorf("jq: %w", ctxErr)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("jq: query failed: %v", err)
|
||||
}
|
||||
|
||||
|
|
|
|||
59
util/jq/jq_test.go
Normal file
59
util/jq/jq_test.go
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
package jq
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/itchyny/gojq"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestQuery(t *testing.T) {
|
||||
query, err := gojq.Parse(".foo")
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err := Query(query, []byte(`{"foo": 42}`))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, float64(42), res)
|
||||
}
|
||||
|
||||
// TestQueryContextTimeout ensures non-terminating queries are aborted instead of
|
||||
// running forever, exhausting cpu or memory
|
||||
func TestQueryContextTimeout(t *testing.T) {
|
||||
for _, q := range []string{
|
||||
`[range(1e9)]`, // unbounded allocation
|
||||
`def f: f; f`, // unbounded recursion
|
||||
`[repeat(0)]`, // infinite generator
|
||||
} {
|
||||
t.Run(q, func(t *testing.T) {
|
||||
query, err := gojq.Parse(q)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := QueryContext(ctx, query, []byte(`{}`))
|
||||
done <- err
|
||||
}()
|
||||
|
||||
select {
|
||||
case err := <-done:
|
||||
assert.Error(t, err)
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("query did not terminate")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryContextHalt(t *testing.T) {
|
||||
query, err := gojq.Parse(`halt`)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = Query(query, []byte(`{}`))
|
||||
assert.Error(t, err)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue