Skip to content

PreventFilter and PreventSort bypass through a query-name collision #132

Closed

Description

@pdevito3

Summary

Two Prevent* checks can look at one member while the query uses a different member. The cause is a query name that is the same as another member name (any letter case). As a result, a client can filter, probe, or sort by a prevented property. The consumer gets no warning.

1. PreventFilter bypass on the right side of a comparison (High)

  • Files: QueryKit/FilterParser.cs (right-side check and CreateRightPropertyExpr, IsPropertyPath).
  • The right-side check calls PropertyResolver.Resolve(...) and examines CanFilter on the mapped member. Then CreateRightPropertyExpr builds the expression from the raw text, by the real member name.
  • Example configuration: Directions has HasQueryName("title"), and Title has PreventFilter().
    • Sku == Title gives x => x.Sku == x.Title, so the prevented column goes into the SQL.
    • Serving _= Title gives x.Serving.StartsWith(x.Title), a prefix probe of the prevented column.
    • The left side is correct: title == "Pancakes" gives x.Directions == "Pancakes".

2. PreventSort bypass through a double query-name lookup (does not reproduce)

  • File: QueryKit/SortParser.cs.
  • The sort maps the query name, then PropertyResolver.Resolve maps it a second time for the CanSort check. The sort body maps the name only one time. Thus the check and the sort can use different members.
  • Example configuration: Serving has HasQueryName("directions"), and Directions has HasQueryName("title").PreventSort().
    • Correction: title asc does not sort by Directions. On v1.14.2 and main, title asc gives OrderBy(_ => 0), so QueryKit skips the prevented sort. The test used Title in place of Serving, because the test Recipe has no Serving property.

Fix direction

  • Resolve each name one time and build the expression from the same PropertyReference.Path that the check used, for the right side of a filter and for a sort.
  • Add unit and Postgres regression tests for both configurations above.
  • Keep the current result for a right-side collection path such as Title == Ingredients.Name. Support collection property paths on the right side of a filter comparison #131 owns that behavior.
  • Optional follow-up: reject colliding query names when the configuration is built. This can break existing configurations.

History

#129 fixed case 1. v1.14.2 and main already throw ParsingException for Title == Ingredients.Name, so #129 did not change that result. #129 closed so that this issue can take the security fixes separately from #131.

Activity

  1. pdevito3 commented on Oct 16, 2026

    @pdevito3
    OwnerAuthor

    Fixed in v1.16.0 by #192. Each property reference now goes through the prevent check, including the right side of a comparison, and the right side is built from the resolved property.

    With the configuration from this issue (Directions has HasQueryName("title"), Title has PreventFilter()):

    Sku == Title
      1.15.0: x => (x.Sku == x.Title)
      1.16.0: x => (True == True)
    
    Serving _= Title
      1.15.0: x => x.Serving.StartsWith(x.Title)
      1.16.0: x => (True == True)
    
    title == "Pancakes"   (Serving also has HasQueryName("directions"))
      1.15.0: x => (x.Serving == "Pancakes")
      1.16.0: x => (x.Directions == "Pancakes")
    

    The sort case did not reproduce before, and it still sorts correctly in 1.16.0.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions