From 9d5929a73f227f863e61255e5ed1addfedde3cb1 Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Thu, 29 Oct 2026 16:16:34 -0700 Subject: [PATCH] s3vectors: reject empty boolean-operator lists and sub-objects in filters (#12220) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * s3vectors: reject empty boolean-operator lists and sub-objects in filters An empty $and/$or list or a nested empty object rendered '()'' — invalid SQL surfaced as an engine error instead of a clean ValidationException. * s3vectors: reject field operator objects with no operators --- weed/s3api/s3vectors/handlers.go | 9 +++++++++ weed/s3api/s3vectors/handlers_test.go | 19 +++++++++++++------ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/weed/s3api/s3vectors/handlers.go b/weed/s3api/s3vectors/handlers.go index 160d7f9652..c5ad9b6135 100644 --- a/weed/s3api/s3vectors/handlers.go +++ b/weed/s3api/s3vectors/handlers.go @@ -1532,6 +1532,9 @@ func filterExpr(filter map[string]interface{}) (string, error) { if !ok { return "", fmt.Errorf("%s expects a list", key) } + if len(sub) == 0 { + return "", fmt.Errorf("%s expects a non-empty list", key) + } exprs := make([]string, 0, len(sub)) for _, item := range sub { m, ok := item.(map[string]interface{}) @@ -1542,6 +1545,9 @@ func filterExpr(filter map[string]interface{}) (string, error) { if err != nil { return "", err } + if e == "" { + return "", fmt.Errorf("%s expects non-empty objects", key) + } exprs = append(exprs, e) } joiner := " AND " @@ -1684,6 +1690,9 @@ func fieldClause(field string, cond interface{}) (string, error) { return "", fmt.Errorf("unsupported operator %s", op) } } + if len(parts) == 0 { + return "", fmt.Errorf("%s expects at least one operator", field) + } return strings.Join(parts, " AND "), nil default: lit, err := sqlLiteral(cond) diff --git a/weed/s3api/s3vectors/handlers_test.go b/weed/s3api/s3vectors/handlers_test.go index 47fed5f131..e50efa8f9c 100644 --- a/weed/s3api/s3vectors/handlers_test.go +++ b/weed/s3api/s3vectors/handlers_test.go @@ -336,12 +336,19 @@ func TestTranslateFilter(t *testing.T) { } for name, filter := range map[string]map[string]interface{}{ - "unknown operator": {"k": map[string]interface{}{"$bogus": "v"}}, - "exists non-bool": {"k": map[string]interface{}{"$exists": "yes"}}, - "range non-number": {"k": map[string]interface{}{"$gt": "v"}}, - "in non-list": {"k": map[string]interface{}{"$in": "v"}}, - "top-level bad op": {"$bogus": []interface{}{}}, - "nested bad member": {"$and": []interface{}{"x"}}, + "unknown operator": {"k": map[string]interface{}{"$bogus": "v"}}, + "exists non-bool": {"k": map[string]interface{}{"$exists": "yes"}}, + "range non-number": {"k": map[string]interface{}{"$gt": "v"}}, + "in non-list": {"k": map[string]interface{}{"$in": "v"}}, + "top-level bad op": {"$bogus": []interface{}{}}, + "nested bad member": {"$and": []interface{}{"x"}}, + "empty and list": {"$and": []interface{}{}}, + "empty or list": {"$or": []interface{}{}}, + "empty sub-object": {"$and": []interface{}{map[string]interface{}{}}}, + "empty operators": {"k": map[string]interface{}{}}, + "empty among fields": {"k": map[string]interface{}{}, "j": "1"}, + "empty under and": {"$and": []interface{}{map[string]interface{}{ + "k": map[string]interface{}{}, "j": map[string]interface{}{}}}}, } { if _, err := translateFilter(filter); err == nil { t.Fatalf("%s should fail", name)