We have a number of security features blocking access to various OData v4 (Microsoft OData v4 implementation) actions based on user roles and permissions. To some extent it is record-level access rights. These security checks are implemented in controller actions themselves, namely the data is filtered directly in the EF LINQ query filter based SQL joins to user permissions. However, as long as [Queryable(MaxExpansionDepth = 0)]
is not applied everywhere (which we don't want) we have found that any user can tack on $expand
to any URL and get any related entity he pleases, without going through the security checks of the actions defined for each of the standard entity routes. For example, if /OData/Projects/
is accessible to a user but /OData/Projects(5)/Employees/
to access employees associated with project 5 is not accessible because of security checks applied to that route within the body of the controller action implementation, the user can just access /OData/Projects(5)?$expand=Employees($expand=SalaryInfo)
.
There doesn't seem to be any way to block this that I've found, other than blocking navigation properties altogether via MaxExpansionDepth
.
What would be ideal is if, since the OData EDM already knows about entities and the Web API routes they're associated with, therefore all navigation properties could be set up to be fed through the existing OData Web API controller action for that entity. In other words, it would be ideal if /OData/Projects(5)?expand=Employees($expand=SalaryInfo)
was handled by OData itself to "join" to /OData/Employees/
on /OData/Projects(5)
and then to /OData/Employees/Salary
, using those contoller actions, rather than through a SQL join via EF, just by nature of the fact that the EDM knows that those entities map to those routes. This way we can define our authorization logic in exactly one place (controller actions) while still enjoying the $expand syntax flexibility. Am I hoping for something stupidly unreasonable?
I've seen this solution but then the problem becomes we would have separate definitions of security, unless the definition of security in that solution itself invokes the action, even then it's only a blocker and not a conditional data filter as we have implemented.
If not, we will have to settle for peppering [Queryable(MaxExpansionDepth = 0)]
on every single vulnerable API endpoint in our system.