Skip to content

Commit c10a668

Browse files
Merge pull request github#13094 from joefarebrother/csharp-missing-access-control
C#: Add query for missing function level access control
2 parents dbffe54 + a53bf4d commit c10a668

25 files changed

+503
-1
lines changed
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
/** Definitions for the missing function level access control query */
2+
3+
import csharp
4+
import semmle.code.csharp.frameworks.microsoft.AspNetCore
5+
import semmle.code.csharp.frameworks.system.web.UI
6+
import semmle.code.asp.WebConfig
7+
8+
/** A method representing an action for a web endpoint. */
9+
abstract class ActionMethod extends Method {
10+
/**
11+
* Gets a string that can indicate what this method does to determine if it should have an auth check;
12+
* such as its method name, class name, or file path.
13+
*/
14+
string getADescription() {
15+
result =
16+
[
17+
this.getName(), this.getDeclaringType().getBaseClass*().getName(),
18+
this.getDeclaringType().getFile().getRelativePath()
19+
]
20+
}
21+
22+
/** Holds if this method may need an authorization check. */
23+
predicate needsAuth() {
24+
this.getADescription()
25+
.regexpReplaceAll("([a-z])([A-Z])", "$1_$2")
26+
// separate camelCase words
27+
.toLowerCase()
28+
.regexpMatch(".*(edit|delete|modify|admin|superuser).*")
29+
}
30+
31+
/** Gets a callable for which if it contains an auth check, this method should be considered authenticated. */
32+
Callable getAnAuthorizingCallable() { result = this }
33+
34+
/**
35+
* Gets a possible url route that could refer to this action,
36+
* which would be covered by `<location>` configurations specifying a prefix of it.
37+
*/
38+
string getARoute() { result = this.getDeclaringType().getFile().getRelativePath() }
39+
}
40+
41+
/** An action method in the MVC framework. */
42+
private class MvcActionMethod extends ActionMethod {
43+
MvcActionMethod() { this = any(MicrosoftAspNetCoreMvcController c).getAnActionMethod() }
44+
}
45+
46+
/** An action method on a subclass of `System.Web.UI.Page`. */
47+
private class WebFormActionMethod extends ActionMethod {
48+
WebFormActionMethod() {
49+
this.getDeclaringType().getBaseClass+() instanceof SystemWebUIPageClass and
50+
this.getAParameter().getType().getName().matches("%EventArgs")
51+
}
52+
53+
override Callable getAnAuthorizingCallable() {
54+
result = super.getAnAuthorizingCallable()
55+
or
56+
result.getDeclaringType() = this.getDeclaringType() and
57+
result.getName() = "Page_Load"
58+
}
59+
60+
override string getARoute() {
61+
exists(string physicalRoute | physicalRoute = super.getARoute() |
62+
result = physicalRoute
63+
or
64+
exists(string absolutePhysical |
65+
virtualRouteMapping(result, absolutePhysical) and
66+
physicalRouteMatches(absolutePhysical, physicalRoute)
67+
)
68+
)
69+
}
70+
}
71+
72+
/**
73+
* Holds if `virtualRoute` is a URL path
74+
* that can map to the corresponding `physicalRoute` filepath
75+
* through a call to `MapPageRoute`
76+
*/
77+
private predicate virtualRouteMapping(string virtualRoute, string physicalRoute) {
78+
exists(MethodCall mapPageRouteCall, StringLiteral virtualLit, StringLiteral physicalLit |
79+
mapPageRouteCall
80+
.getTarget()
81+
.hasQualifiedName("System.Web.Routing", "RouteCollection", "MapPageRoute") and
82+
virtualLit = mapPageRouteCall.getArgument(1) and
83+
physicalLit = mapPageRouteCall.getArgument(2) and
84+
virtualLit.getValue() = virtualRoute and
85+
physicalLit.getValue() = physicalRoute
86+
)
87+
}
88+
89+
/** Holds if the filepath `route` can refer to `actual` after expanding a '~". */
90+
bindingset[route, actual]
91+
private predicate physicalRouteMatches(string route, string actual) {
92+
route = actual
93+
or
94+
route.charAt(0) = "~" and
95+
exists(string dir | actual = dir + route.suffix(1) + ".cs")
96+
}
97+
98+
/** An expression that indicates that some authorization/authentication check is being performed. */
99+
class AuthExpr extends Expr {
100+
AuthExpr() {
101+
this.(MethodCall)
102+
.getTarget()
103+
.hasQualifiedName("System.Security.Principal", "IPrincipal", "IsInRole")
104+
or
105+
this.(PropertyAccess)
106+
.getTarget()
107+
.hasQualifiedName("System.Security.Principal", "IIdentity", ["IsAuthenticated", "Name"])
108+
or
109+
this.(MethodCall).getTarget().getName().toLowerCase().matches("%auth%")
110+
or
111+
this.(PropertyAccess).getTarget().getName().toLowerCase().matches("%auth%")
112+
}
113+
}
114+
115+
/** Holds if `m` is a method that should have an auth check, and does indeed have one. */
116+
predicate hasAuthViaCode(ActionMethod m) {
117+
m.needsAuth() and
118+
exists(Callable caller, AuthExpr auth |
119+
m.getAnAuthorizingCallable().calls*(caller) and
120+
auth.getEnclosingCallable() = caller
121+
)
122+
}
123+
124+
/** An `<authorization>` XML element. */
125+
class AuthorizationXmlElement extends XmlElement {
126+
AuthorizationXmlElement() {
127+
this.getParent() instanceof SystemWebXmlElement and
128+
this.getName().toLowerCase() = "authorization"
129+
}
130+
131+
/** Holds if this element has a `<deny>` element to deny access to a resource. */
132+
predicate hasDenyElement() { this.getAChild().getName().toLowerCase() = "deny" }
133+
134+
/** Gets the physical filepath of this element. */
135+
string getPhysicalPath() { result = this.getFile().getParentContainer().getRelativePath() }
136+
137+
/** Gets the path specified by a `<location>` tag containing this element, if any. */
138+
string getLocationTagPath() {
139+
exists(LocationXmlElement loc, XmlAttribute path |
140+
loc = this.getParent().(SystemWebXmlElement).getParent() and
141+
path = loc.getAnAttribute() and
142+
path.getName().toLowerCase() = "path" and
143+
result = path.getValue()
144+
)
145+
}
146+
147+
/** Gets a route prefix that this configuration can refer to. */
148+
string getARoute() {
149+
result = this.getLocationTagPath()
150+
or
151+
result = this.getPhysicalPath() + "/" + this.getLocationTagPath()
152+
or
153+
not exists(this.getLocationTagPath()) and
154+
result = this.getPhysicalPath()
155+
}
156+
}
157+
158+
/**
159+
* Holds if the given action has an xml `authorization` tag that refers to it.
160+
*/
161+
predicate hasAuthViaXml(ActionMethod m) {
162+
exists(AuthorizationXmlElement el, string rest |
163+
el.hasDenyElement() and
164+
m.getARoute() = el.getARoute() + rest
165+
)
166+
}
167+
168+
/** Holds if the given action has an attribute that indications authorization. */
169+
predicate hasAuthViaAttribute(ActionMethod m) {
170+
exists(Attribute attr | attr.getType().getName().toLowerCase().matches("%auth%") |
171+
attr = m.getAnAttribute() or
172+
attr = m.getDeclaringType().getABaseType*().getAnAttribute()
173+
)
174+
}
175+
176+
/** Holds if `m` is a method that should have an auth check, but is missing it. */
177+
predicate missingAuth(ActionMethod m) {
178+
m.needsAuth() and
179+
not hasAuthViaCode(m) and
180+
not hasAuthViaXml(m) and
181+
not hasAuthViaAttribute(m) and
182+
exists(m.getBody().getAChildStmt()) // exclude empty methods
183+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
public class ProfileController : Controller {
2+
3+
// BAD: No authorization is used.
4+
public ActionResult Edit(int id) {
5+
...
6+
}
7+
8+
// GOOD: The `Authorize` attribute is used.
9+
[Authorize]
10+
public ActionResult Delete(int id) {
11+
...
12+
}
13+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
6+
<overview>
7+
<p>
8+
Sensitive actions, such as editing or deleting content, or accessing admin pages, should have authorization checks
9+
to ensure that they cannot be used by malicious actors.
10+
</p>
11+
12+
</overview>
13+
<recommendation>
14+
15+
<p>
16+
Ensure that proper authorization checks are made for sensitive actions.
17+
For WebForms applications, the <code>authorization</code> tag in <code>Web.config</code> XML files
18+
can be used to implement access control. The <code>System.Web.UI.Page.User</code> property can also be
19+
used to verify a user's role.
20+
For MVC applications, the <code>Authorize</code> attribute can be used to require authorization on specific
21+
action methods.
22+
</p>
23+
24+
</recommendation>
25+
<example>
26+
27+
<p>
28+
In the following WebForms example, the case marked BAD has no authorization checks whereas the
29+
case marked GOOD uses <code>User.IsInRole</code> to check for the user's role.
30+
</p>
31+
32+
<sample src="WebForms.cs" />
33+
34+
<p>
35+
The following <code>Web.config</code> file uses the <code>authorization</code> tag to deny access to anonymous users,
36+
in a <code>location</code> tag to have that configuration apply to a specific path.
37+
</p>
38+
39+
<sample src="Web.config" />
40+
41+
<p>
42+
In the following MVC example, the case marked BAD has no authorization
43+
checks whereas the case marked GOOD uses the <code>Authorize</code> attribute.
44+
</p>
45+
46+
<sample src="MVC.cs" />
47+
48+
</example>
49+
<references>
50+
<li><code>Page.User</code> Property - <a href="https://learn.microsoft.com/en-us/dotnet/api/system.web.ui.page.user?view=netframework-4.8.1#system-web-ui-page-user">Microsoft Learn</a>.</li>
51+
<li>Control authorization permissions in an ASP.NET application - <a href="https://learn.microsoft.com/en-us/troubleshoot/developer/webapps/aspnet/www-authentication-authorization/authorization-permissions">Microsoft Learn</a>.</li>
52+
<li>Simple authorization in ASP.NET Core - <a href="https://learn.microsoft.com/en-us/aspnet/core/security/authorization/simple?view=aspnetcore-7.0">Microsoft Learn</a>.</li>
53+
</references>
54+
</qhelp>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* @name Missing function level access control
3+
* @description Sensitive actions should have authorization checks to prevent them from being used by malicious actors.
4+
* @kind problem
5+
* @problem.severity warning
6+
* @security-severity 7.5
7+
* @precision medium
8+
* @id cs/web/missing-function-level-access-control
9+
* @tags security
10+
* external/cwe/cwe-285
11+
* external/cwe/cwe-284
12+
* external/cwe/cwe-862
13+
*/
14+
15+
import csharp
16+
import semmle.code.csharp.security.auth.MissingFunctionLevelAccessControlQuery
17+
18+
from Method m
19+
where missingAuth(m)
20+
select m, "This action is missing an authorization check."
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?xml version="1.0"?>
2+
3+
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
4+
<location path="User/Profile">
5+
<system.web>
6+
<authorization>
7+
<deny users="?" />
8+
</authorization>
9+
</system.web>
10+
</location>
11+
</configuration>
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
class ProfilePage : System.Web.UI.Page {
2+
// BAD: No authorization is used
3+
protected void btn1_Edit_Click(object sender, EventArgs e) {
4+
...
5+
}
6+
7+
// GOOD: `User.IsInRole` checks the current user's role.
8+
protected void btn2_Delete_Click(object sender, EventArgs e) {
9+
if (!User.IsInRole("admin")) {
10+
return;
11+
}
12+
...
13+
}
14+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: newQuery
3+
---
4+
* Added a new query, `cs/web/missing-function-level-access-control`, to find instances of missing authorization checks.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
| ProfileController.cs:9:25:9:31 | Delete1 | This action is missing an authorization check. |
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Security Features/CWE-285/MissingAccessControl.ql
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using Microsoft.AspNetCore.Mvc;
2+
using Microsoft.AspNetCore.Authorization;
3+
4+
public class ProfileController : Controller {
5+
private void doThings() { }
6+
private bool isAuthorized() { return false; }
7+
8+
// BAD: This is a Delete method, but no auth is specified.
9+
public ActionResult Delete1(int id) {
10+
doThings();
11+
return View();
12+
}
13+
14+
// GOOD: isAuthorized is checked.
15+
public ActionResult Delete2(int id) {
16+
if (!isAuthorized()) {
17+
return null;
18+
}
19+
doThings();
20+
return View();
21+
}
22+
23+
// GOOD: The Authorize attribute is used.
24+
[Authorize]
25+
public ActionResult Delete3(int id) {
26+
doThings();
27+
return View();
28+
}
29+
30+
}

0 commit comments

Comments
 (0)