Skip to content
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

Missing cross-site request forgery token validation query (experimental) #16942

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/** Library to support cs/web/missing-token-validation-aspnetcore query */

private import csharp
private import semmle.code.csharp.frameworks.system.Web
private import semmle.code.csharp.frameworks.system.web.Helpers as Helpers
private import semmle.code.csharp.frameworks.system.web.Mvc as Mvc
private import semmle.code.csharp.frameworks.microsoft.AspNetCore as AspNetCore

/** Defines classes used for anti-forgery tokens. */

Check warning

Code scanning / CodeQL

Class QLDoc style. Warning

The QLDoc for a class should start with 'A', 'An', or 'The'.
class AntiForgeryClass extends Class {
AntiForgeryClass() {
this instanceof Helpers::AntiForgeryClass or
this instanceof AspNetCore::AntiForgeryClass
}

/** Gets the `Validate` method. */
Method getValidateMethod() {
result = this.(Helpers::AntiForgeryClass).getValidateMethod() or
result = this.(AspNetCore::AntiForgeryClass).getValidateMethod()
}
}

/** Defines the anti-forgery token attribute. */

Check warning

Code scanning / CodeQL

Class QLDoc style. Warning

The QLDoc for a class should start with 'A', 'An', or 'The'.
class ValidateAntiForgeryTokenAttribute extends Attribute {
ValidateAntiForgeryTokenAttribute() {
this instanceof Mvc::ValidateAntiForgeryTokenAttribute or
this instanceof AspNetCore::ValidateAntiForgeryAttribute
}
}

/** Defines a generic controller including Mvc and AspNetCore */

Check warning

Code scanning / CodeQL

Class QLDoc style. Warning

The QLDoc for a class should start with 'A', 'An', or 'The'.
class Controller extends Class {
Controller() {
this instanceof Mvc::Controller or
this instanceof AspNetCore::MicrosoftAspNetCoreMvcController
}

/** Gets a Method with a POST action */
Method getAPostActionMethod() {
result = this.(Mvc::Controller).getAPostActionMethod()
or
exists(Method method |
method = this.(AspNetCore::MicrosoftAspNetCoreMvcController).getAnActionMethod() and
method.getAnAttribute() instanceof AspNetCore::MicrosoftAspNetCoreMvcHttpPostAttribute and
result = method
)
}
}

/** An `AuthorizationFilter` that calls the `AntiForgery.Validate` method. */
class AntiForgeryAuthorizationFilter extends Mvc::AuthorizationFilter {
AntiForgeryAuthorizationFilter() {
this.getOnAuthorizationMethod().calls*(any(AntiForgeryClass a).getValidateMethod())
}
}

/** Find a filter that auto-validates anti-forgery tokens. */

Check warning

Code scanning / CodeQL

Class QLDoc style. Warning

The QLDoc for a class should start with 'A', 'An', or 'The'.
class AutoValidateAntiForgeryTokenFilter extends Expr {
AutoValidateAntiForgeryTokenFilter() {
exists(
MethodCall addControllers, LambdaExpr lambda, ParameterAccess options, PropertyCall filters,
MethodCall add
|
// "AddMvc", "AddControllersWithViews", "AddMvcCore", "AddControllers", "AddRazorPages", so generalised to "Add*" to future-proof
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The the second argument of AddRazorPages is of type Action<RazorPageOptions> and RazorPageOptions doesn't have a Filter property.
Should the logic also check that the first parameter to the lambda is of type MvcOptions?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If all the methods are in Microsoft.Extensions.DependencyInjection then maybe we should check the qualifier?

addControllers.getTarget().getName().matches("Add%") and
addControllers.getArgument(1) = lambda and
lambda.getAParameter().getAnAccess() = options and
filters.getQualifier() = options and
filters.getTarget().getName() = "get_Filters" and
add.getQualifier() = filters and
add.getTarget().getUndecoratedName() = "Add" and
this = add and
(
// new AutoValidateAntiforgeryTokenAttribute()
exists(ObjectCreation obj |
add.getArgument(0) = obj and
obj.getType() instanceof AutoValidateAntiforgeryTokenAttributeType
)
or
// typeof(AutoValidateAntiforgeryTokenAttribute)
exists(TypeAccess access |
add.getArgument(0).(TypeofExpr).getAChild() = access and
access.getType() instanceof AutoValidateAntiforgeryTokenAttributeType
)
or
// Add<AutoValidateAntiforgeryTokenAttribute>()
add.getTarget().getName() = "Add<AutoValidateAntiforgeryTokenAttribute>"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also cover the method Add<TFilterType>(Int32)?

)
)
}
}

/** Accounts for custom auto-validation classes with a similar name */

Check warning

Code scanning / CodeQL

Class QLDoc style. Warning

The QLDoc for a class should start with 'A', 'An', or 'The'.
class AutoValidateAntiforgeryTokenAttributeType extends Type {
AutoValidateAntiforgeryTokenAttributeType() {
this.getName().matches("%AutoValidateAntiforgeryTokenAttribute")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use hasFullyQualifiedName instead (as far as I can tell from the documentation we can pinpoint the type and attribute exactly.

}
}

/** The ignore anti-forgery token attribute. */
class IgnoreAntiforgeryTokenAttribute extends Attribute {
IgnoreAntiforgeryTokenAttribute() { this.getType().getName() = "IgnoreAntiforgeryTokenAttribute" }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use hasFullyQualifiedName instead.

}

/** The auto-validate anti-forgery token attribute. */
class AutoValidateAntiforgeryTokenAttribute extends Attribute {
AutoValidateAntiforgeryTokenAttribute() {
this.getType() instanceof AutoValidateAntiforgeryTokenAttributeType
}
}

/**
* Holds if the project has a global anti forgery filter.
*/
predicate hasGlobalAntiForgeryFilter() {
// A global filter added
exists(MethodCall addGlobalFilter |
// addGlobalFilter adds a filter to the global filter collection
addGlobalFilter.getTarget() = any(Mvc::GlobalFilterCollection gfc).getAddMethod() and
// The filter is an antiforgery filter
addGlobalFilter.getArgumentForName("filter").getType() instanceof AntiForgeryAuthorizationFilter and
// The filter is added by the Application_Start() method
any(WebApplication wa)
.getApplication_StartMethod()
.calls*(addGlobalFilter.getEnclosingCallable())
)
or
// for ASP.NET Core
exists(AutoValidateAntiForgeryTokenFilter filter)
}

/** Holds if the Method has the name "Login" */
predicate isLoginAction(Method m) { m.getName() = "Login" }

/** Holds if a Method has a CSRF attribute. */
predicate methodHasCsrfAttribute(Method method) {
exists(Attribute attribute |
(
attribute instanceof ValidateAntiForgeryTokenAttribute or
attribute instanceof IgnoreAntiforgeryTokenAttribute
) and
(
method.getAnAttribute() = attribute or
method.getAnUltimateImplementee().getAnAttribute() = attribute
)
)
}

/** Holds if a Controller has a CSRF attribute. */
predicate controllerHasCsrfAttribute(Controller c) {
exists(Attribute attribute |
(
attribute instanceof ValidateAntiForgeryTokenAttribute or
attribute instanceof IgnoreAntiforgeryTokenAttribute or
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we allow this? The description of the attribute states that "A filter that skips antiforgery token validation.".

attribute instanceof AutoValidateAntiforgeryTokenAttribute
) and
c.getBaseClass*().getAnAttribute() = attribute
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
category: minorAnalysis
---
* A "Missing cross-site request forgery token validation" query for AspNetCore (`cs/web/missing-token-validation-aspnetcore`) was added as an `experimental` query.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe mention that this query also covers MVC.

* Unlike the existing "Missing cross-site request forgery token validation" query (`cs/web/missing-token-validation`), this query adds support for AspNetCore
* The new query also does not tolerate situations where no CSRF token validation is present, so will produce many more (legitimate) results than the existing query. For this reason alone, it has been published as `experimental`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System.Web.Mvc;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe an ASP example would make sense instead (this is also what is mentioned in the qlhelp).


public class HomeController : Controller
{
// BAD: Anti forgery token has been forgotten
[HttpPost]
public ActionResult Login()
{
return View();
}

// GOOD: Anti forgery token is validated
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult UpdateDetails()
{
return View();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>Web applications that use tokens to prevent cross-site request forgery
(CSRF) should validate the tokens for all HTTP POST requests.</p>

<p>Although login and authentication methods are not vulnerable to traditional
CSRF attacks, they still need to be protected with a token or other mitigation.
This because an unprotected login page can be used by an attacker to force a
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: This is because

login using an account controlled by the attacker. Subsequent requests to the
site are then made using this account, without the user being aware that this is
the case. This can result in the user associating private information with the
attacker-controlled account.</p>
</overview>

<recommendation>
<p>The appropriate attribute should be added to this method to ensure the
anti-forgery token is validated when this action method is called. If using the
MVC-provided anti-forgery framework this will be the
<code>[ValidateAntiForgeryToken]</code> attribute.
</p>
<p>Alternatively, you may consider including a global filter that applies token
validation to all POST requests.</p>

</recommendation>

<example>
<p>In the following example an ASP.NET MVC <code>Controller</code> is using the
<code>[ValidateAntiForgeryToken]</code> attribute to mitigate against CSRF attacks.
It has been applied correctly to the <code>UpdateDetails</code> method. However,
this attribute has not been applied to the <code>Login</code> method. This should
be fixed by adding this attribute.</p>
<sample src="MissingAntiForgeryTokenValidationAspNetCore.cs" />
</example>

<references>
<li>Wikipedia: <a href="https://en.wikipedia.org/wiki/Cross-site_request_forgery">Cross-Site Request Forgery</a>.</li>
<li>Microsoft Docs: <a href="https://docs.microsoft.com/en-us/aspnet/mvc/overview/security/xsrfcsrf-prevention-in-aspnet-mvc-and-web-pages">XSRF/CSRF Prevention in ASP.NET MVC and Web Pages</a>.</li>
</references>
</qhelp>
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* @name Missing cross-site request forgery token validation
* @description Handling a POST request without verifying that the request came from the user
* allows a malicious attacker to submit a request on behalf of the user.
* @kind problem
* @problem.severity error
* @security-severity 8.8
* @precision high
* @id cs/web/missing-token-validation-aspnetcore
* @tags security
* external/cwe/cwe-352
* experimental
*/

import csharp
import experimental.code.csharp.MissingAntiForgeryTokenValidationAspNetCore

from Method postMethod, Controller c
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The existing query assumes that if none of the attributes to ensure token validation is identified, other means of validation are performed (contrary to this query). Do you think that was a bad assumption on the original query?

where
postMethod = c.getAPostActionMethod() and
// The method is not protected by a "validate anti forgery token" attribute (or ignores it)
not methodHasCsrfAttribute(postMethod) and
// the class of the method doesn't have a "validate anti forgery token" method (or ignores it)
not controllerHasCsrfAttribute(c) and
// a global anti forgery filter is not in use
not hasGlobalAntiForgeryFilter()
select postMethod,
"Method '" + postMethod.getName() +
"' handles a POST request without performing CSRF token validation."
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System.Web.Mvc;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add some more tests.
(1) Since ASP.NET is now covered by this query, then maybe add a test for an ASP like controller (using Microsoft.AspNetCore.Mvc).
(2) Maybe add a test where one of the CSRF attributes is added to the controller (or a super type of the controller). (I know that this is not covered by the test for the original query - but it would be great to cover it as well).
(3) The most complicated logic is in the "global" add filter logic - I think it would be great to add some tests for this as well.


public class HomeController : Controller
{
// BAD: Anti forgery token has been forgotten
[HttpPost]
public string Login()
{
return View();
}

// GOOD: Anti forgery token is validated
[HttpPost]
[ValidateAntiForgeryToken]
public string UpdateDetails()
{
return View();
}

// Dummy View method to allow compilation
protected string View()
{
// Simulate returning a view. In a real application, this would return an actual view using a subclass of ActionResult.
return "This is a dummy view.";
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
| MissingAntiForgeryTokenValidationAspNetCore.cs:7:19:7:23 | Login | Method 'Login' handles a POST request without performing CSRF token validation. |
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
experimental/CWE-352/MissingAntiForgeryTokenValidationAspNetCore.ql
4 changes: 4 additions & 0 deletions csharp/ql/test/experimental/CWE-352/options
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
semmle-extractor-options: /nostdlib /noconfig
semmle-extractor-options: --load-sources-from-project:${testdir}/../../resources/stubs/_frameworks/Microsoft.NETCore.App/Microsoft.NETCore.App.csproj
semmle-extractor-options: --load-sources-from-project:${testdir}/../../resources/stubs/_frameworks/Microsoft.AspNetCore.App/Microsoft.AspNetCore.App.csproj
semmle-extractor-options: ${testdir}/../../resources/stubs/System.Web.cs
Loading