diff --git a/app/controlplane/pkg/data/workflow.go b/app/controlplane/pkg/data/workflow.go index 31918e88d..9fde4ef5f 100644 --- a/app/controlplane/pkg/data/workflow.go +++ b/app/controlplane/pkg/data/workflow.go @@ -279,10 +279,7 @@ func (r *WorkflowRepo) List(ctx context.Context, orgID uuid.UUID, filter *biz.Wo wfQuery := baseQuery.Where(workflow.DeletedAtIsNil()) // Apply additional filters to the Workflow query based on the provided options - wfQuery, err := applyWorkflowFilters(wfQuery, filter) - if err != nil { - return nil, 0, err - } + wfQuery = applyWorkflowFilters(wfQuery, filter) // Get the count of all filtered rows without the limit and offset count, err := wfQuery.Count(ctx) @@ -340,7 +337,7 @@ func applyWorkflowRunFilters(baseQuery *ent.WorkflowQuery, opts *biz.WorkflowLis } // applyWorkflowFilters applies filters to the Workflow query based on the provided options -func applyWorkflowFilters(wfQuery *ent.WorkflowQuery, opts *biz.WorkflowListOpts) (*ent.WorkflowQuery, error) { +func applyWorkflowFilters(wfQuery *ent.WorkflowQuery, opts *biz.WorkflowListOpts) *ent.WorkflowQuery { if opts != nil { if opts.ProjectIDs != nil { wfQuery = wfQuery.Where(workflow.ProjectIDIn(opts.ProjectIDs...)) @@ -361,20 +358,15 @@ func applyWorkflowFilters(wfQuery *ent.WorkflowQuery, opts *biz.WorkflowListOpts // Append the JSON Filters to the query if len(opts.JSONFilters) != 0 { - // Build the predicates for each JSON filter up front so that any - // validation error (e.g. an unsafe field path) is surfaced instead - // of being silently ignored inside the query builder closure. - predicates := make([]*sql.Predicate, 0, len(opts.JSONFilters)) - for _, filter := range opts.JSONFilters { - // Include the column where the filter is applied - filter.Column = workflow.FieldMetadata - jsonPredicate, err := jsonfilter.BuildEntSelectorFromJSONFilter(filter) - if err != nil { - return nil, biz.NewErrValidation(fmt.Errorf("invalid JSON filter: %w", err)) - } - predicates = append(predicates, jsonPredicate) - } wfQuery = wfQuery.Where(func(selector *sql.Selector) { + // Build the predicates for each JSON filter + predicates := make([]*sql.Predicate, 0, len(opts.JSONFilters)) + for _, filter := range opts.JSONFilters { + // Include the column where the filter is applied + filter.Column = workflow.FieldMetadata + jsonPredicate, _ := jsonfilter.BuildEntSelectorFromJSONFilter(filter) + predicates = append(predicates, jsonPredicate) + } // Combine the predicates using AND logic selector.Where(sql.And(predicates...)) }) @@ -398,7 +390,7 @@ func applyWorkflowFilters(wfQuery *ent.WorkflowQuery, opts *biz.WorkflowListOpts } } - return wfQuery, nil + return wfQuery } // GetOrgScoped Gets a workflow making sure it belongs to a given org diff --git a/pkg/jsonfilter/jsonfilter.go b/pkg/jsonfilter/jsonfilter.go index 2ff52fe4b..40d26b969 100644 --- a/pkg/jsonfilter/jsonfilter.go +++ b/pkg/jsonfilter/jsonfilter.go @@ -25,17 +25,6 @@ import ( "entgo.io/ent/dialect/sql/sqljson" ) -// fieldPathRegexp matches a safe JSON field path expressed in dot notation. -// A path is a dot-separated sequence of identifiers (starting with a letter or -// underscore) optionally followed by numeric array indices, e.g. "name", -// "labels.env" or "items[0].name". -// -// This allowlist is security-critical: the underlying ent sqljson helpers -// concatenate path segments verbatim into single-quoted PostgreSQL JSON path -// literals without escaping, so any character outside this set (e.g. a single -// quote) would allow breaking out of the literal and injecting arbitrary SQL. -var fieldPathRegexp = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*|\[[0-9]+\])*$`) - // columnRegexp matches a safe SQL column identifier (a single unqualified // identifier such as "metadata"). // @@ -83,13 +72,6 @@ func BuildEntSelectorFromJSONFilter(jsonFilter *JSONFilter) (*entsql.Predicate, return nil, err } - // Validate the field path before it reaches the SQL builder. The ent - // sqljson helpers concatenate the path segments unescaped into the query, - // so an unsafe value would allow SQL injection. - if err := validateFieldPath(jsonFilter.FieldPath); err != nil { - return nil, err - } - // Convert the dot notation to the path that Ent expects. dotPath := sqljson.DotPath(jsonFilter.FieldPath) @@ -133,18 +115,3 @@ func validateColumn(column string) error { return nil } - -// validateFieldPath ensures the JSON field path only contains safe characters -// before it is concatenated into the SQL query by the ent sqljson helpers. -// An empty path is allowed: it targets the column itself and is not injectable. -func validateFieldPath(fieldPath string) error { - if fieldPath == "" { - return nil - } - - if !fieldPathRegexp.MatchString(fieldPath) { - return fmt.Errorf("invalid field path %q: must be dot-separated identifiers with optional numeric array indices", fieldPath) - } - - return nil -}