Protect your Queryable API with the validation feature in ASP.NET Web API OData

Hongmei Ge

In the previous blog post, you can see how easy it is to enable OData query syntax for a particular action using Web API OData. Simply add a Queryable attribute to your action as follows, and you are done.

   1: [Queryable]

   2: public IQueryable<WorkItem> Get(int projectId)

It not only works for those actions using OData format, but also applies to any vanilla web api actions using other formats such as JSON.NET. It greatly reduces the need to write a lot of actions to perform operations like top, skip, orderby as well as filter. It is so powerful that it is almost hard to imagine building a real Web API application without it.

Now everything works beautifully, why validation?

As great as this may sound, adding Queryable to your action could expose your service to a DOS attack. A very complex query could take a long time to complete and burn many CPU cycles, with the unwanted side effect of blocking access to the entire service. For example, sorting a large set of records with a non indexed column could take a long time. So it makes sense to restrict the properties used in $orderby.

Also keep in mind if your queryable action is only exposed within your organization, you are probably fine given all your requests come from trusted clients. However, as soon as you expose your queryable action outside your organization, you must take extra caution. One way to protect your service is to validate your query and reject the bad ones upfront. 

In short, if your action returns a large set of data, and your action can be accessed by untrusted clients, it is strongly recommended that you add a layer of defense to your service using the new validation feature. In this blog post, I will go through a few common user scenarios and show you how your queryable action can be protected.

Basic Scenarios

Starting with the Web API OData RC release, we have added some really convenient properties on QueryableAttribute to help with validating the incoming queries. Let us go through them one by one.

Scenario 1: Only allow $top and $skip

A lot of time, all you want to support is client driven paging. In that case, you can limit your action to only allow query that contains $top and $skip. None of the advanced features in $orderby or $filter matter to you. This can be easily done using an simple property called AllowedQueryOptions.

   1: [Queryable(AllowedQueryOptions = AllowedQueryOptions.Skip | AllowedQueryOptions.Top)]

   2: public IQueryable<WorkItem> Get(int projectId)

Now with this, a request to http://localhost/api/workitems/?$top=2&$skip=3 works fine, but a request to http://localhost/api/workitems/?$orderby=Description or http://localhost/api/workitems/?$filter=Id eq 123 will receive a response of 400 Bad Request.

Scenario 2: Limit the value for $top to 100

In the client driven paging scenario, sometimes the server might want to limit the maximum number of records that the client wants to request using $top. You can use MaxTop property in this case.

   1: [Queryable(MaxTop = 100)]

   2: public IQueryable<WorkItem> Get(int projectId)

Now a request to http://localhost/api/workitems/?$top=120 will fail since it exceeds the limit of 100.

Scenario 3: Limit the value for $skip to 200 

Similar to the previous scenario, server wants to limit the maximum number of records to be skipped. You can use MaxSkip property to achieve that.

   1: [Queryable(MaxSkip = 200)]

   2: public IQueryable<WorkItem> Get(int projectId)

Now a request to http://localhost/api/workitems/?$skip=220 will fail since it exceeds the limit of 200.

Scenario 4: Only want to order the results by Id property, nothing else

A lot of times people only want to order the results with a fixed number of properties. Order by any arbitrary properties could be slow and unwanted. Now you have a simple way to do that using AllowedOrderbyProperties.

   1: [Queryable(AllowedOrderByProperties = "Id")]

   2: public IQueryable<WorkItem> Get(int projectId)

Now a request to http://localhost/api/workitems/?$orderby=Description will fail since it can’t be ordered by Description property.

Scenario 5: Only need eq comparison in $filter

$filter is one of the most powerful query options you can apply to the results. If you know that your trusted clients only uses equal comparison inside the $filter. You should validate that as well using AllowedLogicalOperators. Here is how you can do it.

   1: [Queryable(AllowedLogicalOperators = AllowedLogicalOperators.Equal)]

   2: public IQueryable<WorkItem> Get(int projectId)

Now a request to http://localhost/api/workitems/?$filter=Id eq 100 will succeed, while a request to http://localhost/api/workitems/?$filter=Id gt 100 will fail since it does not allow gt.

Scenario 6: Do not need any of the arithmetic operations in $filter

OData URL convention supports a lot of convenient arithmetic operations. However, it is possible your scenario don’t need to use any of those. Now you can turn that off by setting AllowedArithmeticOperators to None.

   1: [Queryable(AllowedArithmeticOperators = AllowedArithmeticOperators.None)]

   2: public IQueryable<WorkItem> Get(int projectId)

Now a request to http://localhost/api/workitems/?$filter=Votes gt 20 will succeed, while a request to http://localhost/api/workitems/?$filter=Votes mul 2 gt 20 will fail since this action does not allow multiplication, which is one of the arithmetic operations.

Scenario 7: Only need to use StartsWith function in $filter

One of the reasons $filter is powerful is that it supports a lot of useful functions. You can read the whole list at the official odata spec. Now oftentimes you only use very few functions, so again you can limit that as well using AllowedFunctions property.

   1: [Queryable(AllowedFunctions = AllowedFunctions.StartsWith)]

   2: public IQueryable<WorkItem> Get(int projectId)

Now a request to http://localhost/api/workitems/?$filter=startswith(Name, ‘A’) will succeed, while a request to http://localhost/api/workitems/?$filter=length(Name) lt 10 will fail because this action does not allow length function. 

Advanced Scenarios

If you can’t restrict the queries via those out of box properties described in the previous section, then you are NOT out of luck. You can use our extensibility point to add your own validation logic easily.

Scenario 8: How to customize default validation logic for $skip, $top, $orderby, $filter

The actual validation logic for those four query options can be extended by overriding their validator classes respectively. That includes TopQueryValidator, SkipQueryValidator, OrderbyQueryValidator and FilterQueryValidator.

The FilterQueryValidator in particular has a lot of virtual methods to help you walk through the Abstract Syntax Tree ( AST ) that is being generated by processing the incoming request.

Here is an example of how you can restrict properties used inside $filter. First you override the existing FilterQueryValidator and override the ValidateSingleValuePropertyAccessNode method so you can examine the PropertyAccessNode, and throw if its Name property has something you don’t like to see.

   1:         public class RestrictiveFilterByQueryValidator : FilterQueryValidator

   2:         {   

   3:             public override void ValidateSingleValuePropertyAccessNode(SingleValuePropertyAccessNode propertyAccessNode, ODataValidationSettings settings)

   4:             {

   5:                 // Validate if we are accessing some sensitive property of WorkItem, such as Votes

   6:                 if (propertyAccessNode.Property.Name == "Votes")

   7:                 {

   8:                     throw new ODataException("Filter with Votes is not allowed.");

   9:                 }

  10:  

  11:                 base.ValidateSingleValuePropertyAccessNode(propertyAccessNode, settings);

  12:             }

  13:         }

Then you can wire your custom validator up via a custom QueryableAttribute.

   1: public class MyQueryableAttribute : QueryableAttribute

   2: {

   3:     public override void ValidateQuery(System.Net.Http.HttpRequestMessage request, ODataQueryOptions queryOptions)

   4:     {

   5:         if (queryOptions.Filter != null)

   6:         {

   7:             queryOptions.Filter.Validator = new RestrictiveFilterByQueryValidator();

   8:         }

   9:  

  10:         base.ValidateQuery(request, queryOptions);

  11:     }

  12: }

The last step is to use your custom QueryableAttribute to decorate your action.

   1: [MyQueryable]

   2: public IQueryable<WorkItem> Get(int projectId)

Now a request to http://localhost/api/workitems/?$filter=Votes eq 20 will fail as the custom validation does not like filtering using Votes property.

Scenario 9: Use ODataQueryOptions to validate the query only

One of the hidden jewels in Web API OData queryable support is this low level API called ODataQueryOptions. If you want to have your own fine control over when the query option is validated or when the query is applied. You can use this instead of QueryableAttribute. Here is how your action will look like with ODataQueryOptions manually applied and your action will return the actual count of the queryable result.

   1: public int GetCount(ODataQueryOptions<WorkItem> queryOptions)

   2: {

   3:     // Validate

   4:     queryOptions.Validate(new ODataValidationSettings() { AllowedQueryOptions = AllowedQueryOptions.Top | AllowedQueryOptions.Skip });

   5:  

   6:     // you can apply your query without using queryOptions at all 

   7:     IQueryable<WorkItem> result = queryOptions.ApplyTo(originalResult) as IQueryable<WorkItem>;

   8:  

   9:     return result.Count();

  10: }

Now you can see one can easily call Validate without calling ApplyTo in the above example. And you can retrieve the raw query value as string and implement your own ApplyTo logic.

In this world, if you have a custom validator, you can also set it on queryOptions directly. Here is how that could look like.

   1: public int Get(ODataQueryOptions<WorkItem> queryOptions)

   2: {

   3:     // Validate using custom validator

   4:     queryOptions.Filter.Validator = new RestrictiveFilterByQueryValidator();

   5:     queryOptions.Validate(new ODataValidationSettings());

   6:  

   7:     // you can apply your query without using queryOptions

   8:     IQueryable<WorkItem> result = queryOptions.ApplyTo(originalResult) as IQueryable<WorkItem>;

   9:  

  10:     return result.Count();

  11: }

To conclude, ASP.NET Web API OData has not only made it super easy to enable OData query syntax on your actions, it also provides you with tools to help protect your queryable actions with upfront validation so that undesired and untrusted queries will be rejected. For more information on ASP.NET Web API OData security, please visit here. Happy coding!

0 comments

Discussion is closed.

Feedback usabilla icon