-
Notifications
You must be signed in to change notification settings - Fork 2.1k
C#: Add query for Insecure Direct Object Reference #13882
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a510a7b
5d12896
251f875
2edd73e
20d42df
009a7bf
f8b1b38
3e6750b
4967fe0
9f25c71
86abd33
a022893
0a27da0
ac45050
6a95ed6
a2dce6b
6d704be
68ad5b7
eb2f589
868836e
475fe3a
4497e22
df5fcc9
3efbbb3
d7c1be4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| /** Common definitions for queries checking for access control measures on action methods. */ | ||
|
|
||
| import csharp | ||
| import semmle.code.csharp.frameworks.microsoft.AspNetCore | ||
| import semmle.code.csharp.frameworks.system.web.UI | ||
|
|
||
| /** A method representing an action for a web endpoint. */ | ||
| abstract class ActionMethod extends Method { | ||
| /** | ||
| * Gets a string that can indicate what this method does to determine if it should have an auth check; | ||
| * such as its method name, class name, or file path. | ||
| */ | ||
| string getADescription() { | ||
| result = [this.getName(), this.getDeclaringType().getBaseClass*().getName(), this.getARoute()] | ||
| } | ||
|
|
||
| /** Holds if this method may represent a stateful action such as editing or deleting */ | ||
| predicate isEdit() { | ||
| exists(string str | | ||
| str = | ||
| this.getADescription() | ||
| // separate camelCase words | ||
| .regexpReplaceAll("([a-z])([A-Z])", "$1_$2") and | ||
| str.regexpMatch("(?i).*(edit|delete|modify|change).*") and | ||
|
mbg marked this conversation as resolved.
|
||
| not str.regexpMatch("(?i).*(on_?change|changed).*") | ||
| ) | ||
| } | ||
|
|
||
| /** Holds if this method may be intended to be restricted to admin users */ | ||
| predicate isAdmin() { | ||
| this.getADescription() | ||
| // separate camelCase words | ||
| .regexpReplaceAll("([a-z])([A-Z])", "$1_$2") | ||
| .regexpMatch("(?i).*(admin|superuser).*") | ||
| } | ||
|
|
||
| /** Gets a callable for which if it contains an auth check, this method should be considered authenticated. */ | ||
| Callable getAnAuthorizingCallable() { result = this } | ||
|
|
||
| /** | ||
| * Gets a possible url route that could refer to this action, | ||
| * which would be covered by `<location>` configurations specifying a prefix of it. | ||
| */ | ||
| string getARoute() { result = this.getDeclaringType().getFile().getRelativePath() } | ||
| } | ||
|
|
||
| /** An action method in the MVC framework. */ | ||
| private class MvcActionMethod extends ActionMethod { | ||
| MvcActionMethod() { this = any(MicrosoftAspNetCoreMvcController c).getAnActionMethod() } | ||
| } | ||
|
|
||
| /** An action method on a subclass of `System.Web.UI.Page`. */ | ||
| private class WebFormActionMethod extends ActionMethod { | ||
| WebFormActionMethod() { | ||
| this.getDeclaringType().getBaseClass+() instanceof SystemWebUIPageClass and | ||
| this.getAParameter().getType().getName().matches("%EventArgs") | ||
| } | ||
|
|
||
| override Callable getAnAuthorizingCallable() { | ||
| result = super.getAnAuthorizingCallable() | ||
| or | ||
| pageLoad(result, this.getDeclaringType()) | ||
| } | ||
|
|
||
| override string getARoute() { | ||
| exists(string physicalRoute | physicalRoute = super.getARoute() | | ||
| result = physicalRoute | ||
| or | ||
| exists(string absolutePhysical | | ||
| virtualRouteMapping(result, absolutePhysical) and | ||
| physicalRouteMatches(absolutePhysical, physicalRoute) | ||
| ) | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| pragma[nomagic] | ||
| private predicate pageLoad(Callable c, Type decl) { | ||
| c.getName() = "Page_Load" and | ||
| decl = c.getDeclaringType() | ||
| } | ||
|
|
||
| /** | ||
| * Holds if `virtualRoute` is a URL path | ||
| * that can map to the corresponding `physicalRoute` filepath | ||
| * through a call to `MapPageRoute` | ||
| */ | ||
| private predicate virtualRouteMapping(string virtualRoute, string physicalRoute) { | ||
| exists(MethodCall mapPageRouteCall, StringLiteral virtualLit, StringLiteral physicalLit | | ||
| mapPageRouteCall | ||
| .getTarget() | ||
| .hasQualifiedName("System.Web.Routing", "RouteCollection", "MapPageRoute") and | ||
| virtualLit = mapPageRouteCall.getArgument(1) and | ||
| physicalLit = mapPageRouteCall.getArgument(2) and | ||
| virtualLit.getValue() = virtualRoute and | ||
| physicalLit.getValue() = physicalRoute | ||
| ) | ||
| } | ||
|
|
||
| /** Holds if the filepath `route` can refer to `actual` after expanding a '~". */ | ||
| bindingset[route, actual] | ||
| private predicate physicalRouteMatches(string route, string actual) { | ||
| route = actual | ||
| or | ||
| route.charAt(0) = "~" and | ||
| exists(string dir | actual = dir + route.suffix(1) + ".cs") | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| /** Definitions for the Insecure Direct Object Reference query */ | ||
|
|
||
| import csharp | ||
| import semmle.code.csharp.dataflow.flowsources.Remote | ||
| import ActionMethods | ||
|
|
||
| /** | ||
| * Holds if `m` is a method that may require checks | ||
| * against the current user to modify a particular resource. | ||
| */ | ||
| // We exclude admin methods as it may be expected that an admin user should be able to modify any resource. | ||
| // Other queries check that there are authorization checks in place for admin methods. | ||
| private predicate needsChecks(ActionMethod m) { m.isEdit() and not m.isAdmin() } | ||
|
|
||
| /** | ||
| * Holds if `m` has a parameter or access a remote flow source | ||
| * that may indicate that it's used as the ID for some resource | ||
| */ | ||
| private predicate hasIdParameter(ActionMethod m) { | ||
| exists(RemoteFlowSource src | src.getEnclosingCallable() = m | | ||
| src.asParameter().getName().toLowerCase().matches(["%id", "%idx"]) | ||
| or | ||
| // handle cases like `Request.QueryString["Id"]` | ||
| exists(StringLiteral idStr, IndexerCall idx | | ||
| idStr.getValue().toLowerCase().matches(["%id", "%idx"]) and | ||
| TaintTracking::localTaint(src, DataFlow::exprNode(idx.getQualifier())) and | ||
| DataFlow::localExprFlow(idStr, idx.getArgument(0)) | ||
| ) | ||
| ) | ||
| } | ||
|
|
||
| private predicate authorizingCallable(Callable c) { | ||
| exists(string name | name = c.getName().toLowerCase() | | ||
| name.matches(["%user%", "%session%"]) and | ||
| not name.matches("%get%by%") // methods like `getUserById` or `getXByUsername` aren't likely to be referring to the current user | ||
| ) | ||
| } | ||
|
|
||
| /** Holds if `m` at some point in its call graph may make some kind of check against the current user. */ | ||
| private predicate checksUser(ActionMethod m) { | ||
| exists(Callable c | | ||
| authorizingCallable(c) and | ||
| callsPlus(m, c) | ||
| ) | ||
| } | ||
|
|
||
| private predicate calls(Callable c1, Callable c2) { c1.calls(c2) } | ||
|
|
||
| private predicate callsPlus(Callable c1, Callable c2) = fastTC(calls/2)(c1, c2) | ||
|
|
||
| /** Holds if `m`, its containing class, or a parent class has an attribute that extends `AuthorizeAttribute` */ | ||
| private predicate hasAuthorizeAttribute(ActionMethod m) { | ||
| exists(Attribute attr | | ||
| attr.getType() | ||
| .getABaseType*() | ||
| .hasQualifiedName([ | ||
| "Microsoft.AspNetCore.Authorization", "System.Web.Mvc", "System.Web.Http" | ||
| ], "AuthorizeAttribute") | ||
| | | ||
| attr = m.getOverridee*().getAnAttribute() or | ||
| attr = m.getDeclaringType().getABaseType*().getAnAttribute() | ||
| ) | ||
| } | ||
|
|
||
| /** Holds if `m`, its containing class, or a parent class has an attribute that extends `AllowAnonymousAttribute` */ | ||
| private predicate hasAllowAnonymousAttribute(ActionMethod m) { | ||
| exists(Attribute attr | | ||
| attr.getType() | ||
| .getABaseType*() | ||
| .hasQualifiedName([ | ||
| "Microsoft.AspNetCore.Authorization", "System.Web.Mvc", "System.Web.Http" | ||
| ], "AllowAnonymousAttribute") | ||
| | | ||
| attr = m.getOverridee*().getAnAttribute() or | ||
| attr = m.getDeclaringType().getABaseType*().getAnAttribute() | ||
| ) | ||
| } | ||
|
|
||
| /** Holds if `m` is authorized via an `Authorize` attribute */ | ||
| private predicate isAuthorizedViaAttribute(ActionMethod m) { | ||
| hasAuthorizeAttribute(m) and | ||
| not hasAllowAnonymousAttribute(m) | ||
|
Comment on lines
+81
to
+82
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am wondering about the correctness of this predicate with different permutations of these attributes in the inheritance chain. E.g. suppose you have a base class with a method that has the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm; I'm not sure what ASP.NET would do in that situation.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I do not know off the top of my head what ASP.NET would do in that case, so I'd need to try it with a sample project as well. I am happy if we postpone looking into this and don't block this PR on addressing this detail. |
||
| } | ||
|
|
||
| /** | ||
| * Holds if `m` is a method that modifies a particular resource based on | ||
| * an ID provided by user input, but does not check anything based on the current user | ||
| * to determine if they should modify this resource. | ||
| */ | ||
| predicate hasInsecureDirectObjectReference(ActionMethod m) { | ||
| needsChecks(m) and | ||
|
mbg marked this conversation as resolved.
|
||
| hasIdParameter(m) and | ||
| not checksUser(m) and | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am concerned by the rate at which this may produce false positives. My understanding is that e.g. in ASP.NET the standard mechanism for managing authentication/authorisation is through attributes. The query does not seem to handle those (as well as auth code inherited from parent classes / any other form of auth), so would this query trigger alerts in those cases?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are authorization attributes alone able to manage authorization that's based on the request parameters?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, using attributes is only sufficient where authorisation is based on roles, policies, etc. For resource-based authorization checks in the body of the handler are indeed necessary. Would the example in that article be identified by However, role-based or policy-based authorisation may be sufficient in a lot of cases, so we probably shouldn't flag up insecure direct object reference vulnerabilities where checks are performed in that way. Based on your description of the MRVA experiment, it seems like the results are only for the top 100 repos? Are those the top 100 C# repos or the top 100 repos that use ASP.NET? In any case, I would probably use a larger sample. It is also likely that the different authorisation mechanisms offered by ASP.NET are more prevalent in enterprise codebases, rather than in open-source ones.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It looks like the example in the article would be identified by However in this top 1000 MRVA run, it looks like there are indeed some FPs from role-based authentication for admin roles; so this is something that should be checked.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Ah, I don't think I had realised that properties count as
Thank you for running that experiment! It does indeed seem to confirm my concerns here.
I think the logic is a little bit more complicated because the In terms of the arguments to the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is my understanding that an
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, although consider that there might be web applications where most visitors have no accounts and the only authorised users you'll get are administrators that should be able to perform such actions. (E.g. a blog, a simple CMS/news website, etc.) In those cases just being logged in may be sufficient access control. Also, for classes that inherit from class MyAuthorizeAttribute : AuthorizeAttribute
{
public MyAuthorizeAttribute()
{
base.Policy = "Administrators";
}
} |
||
| not isAuthorizedViaAttribute(m) and | ||
| exists(m.getBody().getAChildStmt()) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| <!DOCTYPE qhelp PUBLIC | ||
| "-//Semmle//qhelp//EN" | ||
| "qhelp.dtd"> | ||
| <qhelp> | ||
| <overview> | ||
| <p>Resources like comments or user profiles can be accessed and modified through an action method. To target a certain resource, the action method accepts an ID parameter pointing to that specific resource. If the methods do not check that the current user is authorized to access the specified resource, an attacker can access a resource by guessing or otherwise determining the linked ID parameter.</p> | ||
|
|
||
| </overview> | ||
| <recommendation> | ||
| <p> | ||
| Ensure that the current user is authorized to access the resource of the provided ID. | ||
| </p> | ||
|
|
||
| </recommendation> | ||
| <example> | ||
| <p>In the following example, in the "BAD" case, there is no authorization check, so any user can edit any comment for which they guess or determine the ID parameter. | ||
| The "GOOD" case includes a check that the current user matches the author of the comment, preventing unauthorized access.</p> | ||
| <sample src="WebFormsExample.cs" /> | ||
| <p>The following example shows a similar scenario for the ASP.NET Core framework. As above, the "BAD" case provides an example with no authorization check, and the first "GOOD" case provides an example with a check that the current user authored the specified comment. Additionally, in the second "GOOD" case, the `Authorize` attribute is used to restrict the method to administrators, who are expected to be able to access arbitrary resources.</p> | ||
| <sample src="MVCExample.cs" /> | ||
|
|
||
| </example> | ||
| <references> | ||
|
|
||
| <li>OWASP: <a href="https://wiki.owasp.org/index.php/Top_10_2013-A4-Insecure_Direct_Object_References">Insecure Direct Object Refrences</a>.</li> | ||
| <li>OWASP: <a href="https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/05-Authorization_Testing/04-Testing_for_Insecure_Direct_Object_References">Testing for Insecure Direct Object References</a>.</li> | ||
| <li>Microsoft Learn: <a href="https://learn.microsoft.com/en-us/aspnet/core/security/authorization/resourcebased?view=aspnetcore-7.0">Resource-based authorization in ASP.NET Core</a>.</li> | ||
|
|
||
| </references> | ||
| </qhelp> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| /** | ||
| * @name Insecure Direct Object Reference | ||
| * @description Using user input to control which object is modified without | ||
| * proper authorization checks allows an attacker to modify arbitrary objects. | ||
| * @kind problem | ||
| * @problem.severity warning | ||
| * @security-severity 7.5 | ||
| * @precision medium | ||
| * @id cs/web/insecure-direct-object-reference | ||
| * @tags security | ||
| * external/cwe-639 | ||
| */ | ||
|
|
||
| import csharp | ||
| import semmle.code.csharp.security.auth.InsecureDirectObjectReferenceQuery | ||
|
|
||
| from ActionMethod m | ||
| where hasInsecureDirectObjectReference(m) | ||
| select m, | ||
| "This method may be missing authorization checks for which users can access the resource of the provided ID." |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This changed slightly when it was moved. Now
this.getARoute()is being used instead ofthis.getDeclaringType().getFile().getRelativePath()(which is the same as long asgetARouteis not overridden).Is this a bug fix (this might change the description of
WebFormActionMethodelements)?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is an intended fix (the route to a method should also contribute to knowledge about the names referring to that method to help determine what it does)