Skip to content

C#: Add flow steps for View calls refering to Razor pages #14343

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

Merged
merged 28 commits into from
Nov 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
40a7223
Implement xss flow step for absolute filepath case
joefarebrother Sep 27, 2023
12a579e
Add relative filepath lookup
joefarebrother Sep 29, 2023
ac3f642
Unit tests - Write script to aid generating necessary code from .csht…
joefarebrother Oct 9, 2023
4f5ecb8
Add unit tests + fix issue in stubs
joefarebrother Oct 9, 2023
f84b2a9
Add support for view locations defined in code through RazoeViewEngin…
joefarebrother Oct 17, 2023
7691cbc
Add additional test cases
joefarebrother Oct 17, 2023
7194113
Add areas
joefarebrother Oct 23, 2023
f1b0f1a
Use shared filepath normalization libary
joefarebrother Oct 23, 2023
7bd7cc5
Fix tests
joefarebrother Oct 24, 2023
f2c3d83
Add tests for area cases
joefarebrother Oct 24, 2023
826111d
Separate area view discovery list for increased precision
joefarebrother Oct 24, 2023
7371751
Add change note
joefarebrother Oct 24, 2023
0ed7b3c
Update qldoc
joefarebrother Oct 24, 2023
047f8e4
Make the additional flow steps generally applicible to all queries
joefarebrother Oct 26, 2023
2416040
Review suggestions - make import private and update change note
joefarebrother Oct 27, 2023
ef15980
Remove unnecessary check for the name parameter as parameter 1
joefarebrother Oct 27, 2023
96bddde
Review suggestions - Remove unneeded import in tests, rename RazorPag…
joefarebrother Oct 31, 2023
052166f
Fix issue in genfiles.py + add help text
joefarebrother Nov 1, 2023
9af44ed
Convert flow steps to value steps
joefarebrother Nov 7, 2023
7a098dd
Remove AdditionalTaintStep (redundant with NonLocalJumpNode)
joefarebrother Nov 7, 2023
e2e4642
Remove redundant import
joefarebrother Nov 7, 2023
26c048a
Minor refactoring
joefarebrother Nov 7, 2023
82fbae3
Handle standalone extraction case in which generated files list absol…
joefarebrother Nov 9, 2023
33186ac
Add integration tests
joefarebrother Nov 13, 2023
aa3fd6a
Fix standalone tests
joefarebrother Nov 15, 2023
f24c042
Rename Razor Page class to Razor View class
joefarebrother Nov 16, 2023
e4edb19
Update to hasFullyQualifiedName
joefarebrother Nov 16, 2023
befb1cc
Fix integration tests for windows
joefarebrother Nov 16, 2023
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,16 @@
namespace test;

using System.Net;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Razor;

public class UserData
{
public string Name { get; set; }
}

public class TestController : Controller {
public IActionResult Test(UserData tainted1) {
return View("Test", tainted1);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
@page

@model UserData

@if (Model != null)
{
<h3>Hello "@Html.Raw(Model.Name)"</h3>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@using test

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
| Views/Test/Test.cshtml:7:27:7:36 | access to property Name | Controllers/TestController.cs:13:40:13:47 | tainted1 : UserData | Views/Test/Test.cshtml:7:27:7:36 | access to property Name | $@ flows to here and is written to HTML or JavaScript: Microsoft.AspNetCore.Mvc.ViewFeatures.HtmlHelper.Raw() method. | Controllers/TestController.cs:13:40:13:47 | tainted1 : UserData | User-provided value |
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* @name Cross-site scripting
* @description Writing user input directly to a web page
* allows for a cross-site scripting vulnerability.
* @kind path-problem
* @problem.severity error
* @security-severity 6.1
* @precision high
* @id cs/web/xss
* @tags security
* external/cwe/cwe-079
* external/cwe/cwe-116
*/

import csharp
import semmle.code.csharp.security.dataflow.XSSQuery

// import PathGraph // exclude query predicates with output dependant on the absolute filepath the tests are run in
from XssNode source, XssNode sink, string message
where xssFlow(source, sink, message)
select sink, source, sink, "$@ flows to here and " + message, source, "User-provided value"

Check warning

Code scanning / CodeQL

Alert message style violation

Try to use a descriptive phrase instead of "here". Use "this location" if you can't get around mentioning the current location.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import os
from create_database_utils import *


os.environ['CODEQL_EXTRACTOR_CSHARP_STANDALONE_EXTRACT_WEB_VIEWS'] = 'true'
run_codeql_database_create(lang="csharp", extra_args=["--extractor-option=buildless=true", "--extractor-option=cil=false"])
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ private import semmle.code.csharp.controlflow.Guards
private import semmle.code.csharp.dispatch.Dispatch
private import semmle.code.csharp.frameworks.EntityFramework
private import semmle.code.csharp.frameworks.NHibernate
private import semmle.code.csharp.frameworks.Razor
private import semmle.code.csharp.frameworks.system.Collections
private import semmle.code.csharp.frameworks.system.threading.Tasks
private import semmle.code.cil.Ssa::Ssa as CilSsa
Expand Down
217 changes: 217 additions & 0 deletions csharp/ql/lib/semmle/code/csharp/frameworks/Razor.qll
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
/** Provides definitions and flow steps related to Razor pages. */

private import csharp
private import codeql.util.Unit
private import codeql.util.FilePath
private import semmle.code.csharp.frameworks.microsoft.AspNetCore

/** A call to the `View` method */
private class ViewCall extends MethodCall {
ViewCall() {
this.getTarget().hasFullyQualifiedName("Microsoft.AspNetCore.Mvc", "Controller", "View")
}

/** Gets the `name` argument to this call, if any. */
string getNameArgument() {
exists(StringLiteral lit |
this.getTarget().getParameter(0).getType() instanceof StringType and
DataFlow::localExprFlow(lit, this.getArgument(0)) and
result = lit.getValue()
)
}

/** Gets the `model` argument to this call, if any. */
Expr getModelArgument() {
exists(int i | i in [0 .. 1] |
this.getTarget().getParameter(i).getType() instanceof ObjectType and
result = this.getArgument(i)
)
}

/** Gets the MVC action method that this call is made from, if any. */
Method getActionMethod() {
result = this.getEnclosingCallable() and
result = this.getController().getAnActionMethod()
}

/**
* Gets the action name that this call refers to, if any.
* This is either the name argument, or the name of the action method calling this if there is no name argument.
*/
string getActionName() {
result = this.getNameArgument()
or
not exists(this.getNameArgument()) and
result = this.getActionMethod().getName()
Copy link
Contributor

Choose a reason for hiding this comment

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

Can action methods be generic? If so, we will probably need to strip away the <,...,> suffix.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

According to this SO question, action methods cannot be generic.

}

/** Gets the MVC controller that this call is made from, if any. */
MicrosoftAspNetCoreMvcController getController() {
result = this.getEnclosingCallable().getDeclaringType()
}

/** Gets the name of the MVC controller that this call is made from, if any. */
string getControllerName() { result + "Controller" = this.getController().getName() }

/** Gets the name of the Area that the controller of this call belongs to, if any. */
string getAreaName() {
exists(Attribute attr |
attr = this.getController().getAnAttribute() and
attr.getType().hasFullyQualifiedName("Microsoft.AspNetCore.Mvc", "AreaAttribute") and
result = attr.getArgument(0).(StringLiteral).getValue()
)
}

/** `result` is `true` if this call is from a controller that is an Area, and `false` otherwise. */
boolean hasArea() { if exists(this.getAreaName()) then result = true else result = false }
}

/** A compiler-generated Razor page from a `.cshtml` file. */
class RazorViewClass extends Class {
AssemblyAttribute attr;

RazorViewClass() {
exists(Class baseClass | baseClass = this.getBaseClass().getUnboundDeclaration() |
baseClass.hasFullyQualifiedName("Microsoft.AspNetCore.Mvc.Razor", "RazorPage`1")
or
baseClass.hasFullyQualifiedName("Microsoft.AspNetCore.Mvc.RazorPages", "Page")
) and
attr.getFile() = this.getFile() and
attr.getType()
.hasFullyQualifiedName("Microsoft.AspNetCore.Razor.Hosting", "RazorCompiledItemAttribute")
}

/**
* Gets the filepath of the source file that this class was generated from.
*
* This is an absolute path if the database was extracted in standalone mode,
* and is relative to to application root (the directory containing the .csproj file) otherwise.
*/
string getSourceFilepath() { result = attr.getArgument(2).(StringLiteral).getValue() }
}

/**
* Gets a possible prefix to be applied to view search paths to locate a Razor page.
* This may be empty (for the case that the generated Razor page files contain paths relative to the application root),
* or the absolute path of the directory containing the .csproj file (for the case that standalone extraction is used and the generated files contain absolute paths).
*/
private string getARazorPathPrefix() {
result = ""
or
exists(File csproj |
csproj.getExtension() = "csproj" and
// possibly prepend '/' to match Windows absolute paths starting with `C:/` with paths appearing in the Razor file in standalone mode starting with `/C:/`
result = ["/", ""] + csproj.getParentContainer().getAbsolutePath()
)
}

private class ViewCallJumpNode extends DataFlow::NonLocalJumpNode {
RazorViewClass rp;

ViewCallJumpNode() {
exists(ViewCall vc |
viewCallRefersToPage(vc, rp) and
this.asExpr() = vc.getModelArgument()
)
}

override DataFlow::Node getAJumpSuccessor(boolean preservesValue) {
preservesValue = true and
exists(PropertyAccess modelProp |
result.asExpr() = modelProp and
modelProp.getTarget().hasName("Model") and
modelProp.getEnclosingCallable().getDeclaringType() = rp
)
}
}

private predicate viewCallRefersToPage(ViewCall vc, RazorViewClass rp) {
viewCallRefersToPageAbsolute(vc, rp) or
viewCallRefersToPageRelative(vc, rp)
}

bindingset[path]
private string stripTilde(string path) { result = path.regexpReplaceAll("^~/", "/") }

private predicate viewCallRefersToPageAbsolute(ViewCall vc, RazorViewClass rp) {
getARazorPathPrefix() + ["/", ""] + stripTilde(vc.getNameArgument()) = rp.getSourceFilepath()
}

private predicate viewCallRefersToPageRelative(ViewCall vc, RazorViewClass rp) {
rp = min(int i, RazorViewClass rp2 | matchesViewCallWithIndex(vc, rp2, i) | rp2 order by i)
}

private predicate matchesViewCallWithIndex(ViewCall vc, RazorViewClass rp, int i) {
exists(RelativeViewCallFilepath fp |
fp.hasViewCallWithIndex(vc, i) and
getARazorPathPrefix() + fp.getNormalizedPath() = rp.getSourceFilepath()
)
}

/** Gets the `i`th template for view discovery. */
private string getViewSearchTemplate(int i, boolean isArea) {
i = 0 and result = "/Areas/{2}/Views/{1}/{0}.cshtml" and isArea = true
or
i = 1 and result = "/Areas/{2}/Views/Shared/{0}.cshtml" and isArea = true
or
i = 2 and result = "/Views/{1}/{0}.cshtml" and isArea = false
or
i = 3 and result = "/Views/Shared/{0}.cshtml" and isArea = [true, false]
or
i = 4 and result = "/Pages/Shared/{0}.cshtml" and isArea = true
or
i = 5 and result = getAViewSearchTemplateInCode(isArea)
}

/** Gets an additional template used for view discovery defined in code. */
private string getAViewSearchTemplateInCode(boolean isArea) {
exists(StringLiteral str, MethodCall addCall |
addCall.getTarget().hasName("Add") and
DataFlow::localExprFlow(str, addCall.getArgument(0)) and
addCall.getQualifier() = getAViewLocationList(isArea) and
result = str.getValue()
)
}

/** Gets a list expression containing view search locations */
private Expr getAViewLocationList(boolean isArea) {
exists(string name |
result
.(PropertyRead)
.getProperty()
.hasFullyQualifiedName("Microsoft.AspNetCore.Mvc.Razor", "RazorViewEngineOptions", name)
|
name = "ViewLocationFormats" and isArea = false
or
name = "AreaViewLocationFormats" and isArea = true
// PageViewLocationFormats and AreaPageViewLocationFormats are used for calls within a page rather than a controller
)
}

/** A filepath that should be searched for a View call. */
private class RelativeViewCallFilepath extends NormalizableFilepath {
ViewCall vc_;
int idx_;

RelativeViewCallFilepath() {
exists(string template, string sub2, string sub1, string sub0 |
template = getViewSearchTemplate(idx_, vc_.hasArea())
|
(
if template.matches("%{2}%")
then sub2 = template.replaceAll("{2}", vc_.getAreaName())
else sub2 = template
) and
(
if template.matches("%{1}%")
then sub1 = sub2.replaceAll("{1}", vc_.getControllerName())
else sub1 = sub2
) and
sub0 = sub1.replaceAll("{0}", vc_.getActionName()) and
this = stripTilde(sub0)
)
}

/** Holds if this string is the `idx`th path that will be searched for the `vc` call. */
predicate hasViewCallWithIndex(ViewCall vc, int idx) { vc = vc_ and idx = idx_ }
}
4 changes: 4 additions & 0 deletions csharp/ql/src/change-notes/2023-10-24-xss-flow-steps.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
category: minorAnalysis
---
* Modelled additional flow steps to track flow from a `View` call in an MVC controller to the corresponding Razor View (`.cshtml`) file, which may result in additional results for queries such as `cs/web/xss`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@namespace test
@model UserData
@{
}

@if (Model != null)
{
<h3>Hello "@Html.Raw(Model.Name)"</h3>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@namespace test
@model UserData
@{
}

@if (Model != null)
{
<h3>Hello "@Html.Raw(Model.Name)"</h3>
}
Loading