Unit tests - Write script to aid generating necessary code from .cshtml files.

This commit is contained in:
Joe Farebrother
2023-10-09 15:44:57 +01:00
parent 12a579e0aa
commit ac3f642b45
9 changed files with 275 additions and 16 deletions

View File

@@ -1,9 +1,15 @@
/** Definitions for additional flow steps for cross-site scripting (XSS) vulnerabilities. */
import csharp
private import codeql.util.Unit
private import semmle.code.csharp.frameworks.microsoft.AspNetCore
/** An additional flow step for cross-site scripting (XSS) vulnerabilities */
/**
* A unit class for providing additional flow steps for cross-site scripting (XSS) vulnerabilities.
* Extend to provide additional flow steps.
*/
class XssAdditionalFlowStep extends Unit {
/** Holds if there is an additional dataflow step from `node1` to `node2`. */
abstract predicate isAdditionalFlowStep(DataFlow::Node node1, DataFlow::Node node2);
}
@@ -34,6 +40,17 @@ private class ViewCall extends MethodCall {
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()
}
/** Gets the MVC controller that this call is made from, if any. */
MicrosoftAspNetCoreMvcController getController() {
result = this.getEnclosingCallable().getDeclaringType()
@@ -91,31 +108,38 @@ private predicate viewCallRefersToPageRelative(ViewCall vc, RazorPage rp) {
)
}
/** Gets the `i`th template for view discovery. */
private string getViewSearchTemplate(int i) {
i = 0 and result = "/Views/{1}/{0}.cshtml"
or
i = 1 and result = "/Views/Shared/{0}.cshtml"
}
/** A filepath that should be searched for a View call. */
private class RelativeViewCallFilepath extends NormalizableFilepath {
ViewCall vc;
int idx;
ViewCall vc_;
int idx_;
RelativeViewCallFilepath() {
exists(string actionName |
actionName = vc.getNameArgument() and
not actionName.matches("%.cshtml")
exists(string template | template = getViewSearchTemplate(idx_) |
this =
template.replaceAll("{0}", vc_.getActionName()).replaceAll("{1}", vc_.getControllerName())
or
not exists(vc.getNameArgument()) and
actionName = vc.getActionMethod().getName()
|
idx = 0 and
this = "/Views/" + vc.getControllerName() + "/" + actionName + ".cshtml"
or
idx = 1 and
this = "/Views/Shared/" + actionName + ".cshtml"
not exists(vc_.getControllerName()) and
not template.matches("%{1}%") and
this = template.replaceAll("{0}", vc_.getActionName())
)
}
predicate hasViewCallWithIndex(ViewCall vc2, int idx2) { vc = vc2 and idx = idx2 }
/** 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_ }
}
// TODO: this could be a shared library
/** A filepath that should be normalized. */
/**
* A filepath that should be normalized.
* Extend to provide additional strings that should be normalized as filepaths.
*/
abstract private class NormalizableFilepath extends string {
bindingset[this]
NormalizableFilepath() { any() }

View File

@@ -0,0 +1,16 @@
namespace test;
using System.Net;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
public class UserData
{
public string Name { get; set; }
}
public class TestController : Controller {
public IActionResult test1(UserData tainted) {
return View("Test1", tainted);
}
}

View File

@@ -0,0 +1,74 @@
// A test file that mimics the output of compiling a `.cshtml` file
// <auto-generated/>
#pragma warning disable 1591
[assembly: global::Microsoft.AspNetCore.Razor.Hosting.RazorCompiledItemAttribute(typeof(test.Views.$PATHUNDER), @"mvc.1.0.view", @"/$PATHSLASH")]
namespace test.Views
{
#line hidden
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
#nullable restore
using test;
#line default
#line hidden
#nullable disable
[global::Microsoft.AspNetCore.Razor.Hosting.RazorCompiledItemMetadataAttribute("Identifier", "/$PATHSLASH")]
public class $PATHUNDER : global::Microsoft.AspNetCore.Mvc.Razor.RazorPage<UserData>
{
#pragma warning disable 1998
public async override global::System.Threading.Tasks.Task ExecuteAsync()
{
#line 6 "$PATHSLASH"
if (Model != null)
{
#line default
#line hidden
#nullable disable
WriteLiteral(" <h3>Hello \"");
#nullable restore
#line 8 "$PATHSLASH"
Write(Html.Raw(Model.Name));
#line default
#line hidden
#nullable disable
WriteLiteral("\"</h3>\n");
#nullable restore
#line 9 "$PATHSLASH"
}
#line default
#line hidden
#nullable disable
}
#pragma warning restore 1998
#nullable restore
[global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute]
public global::Microsoft.AspNetCore.Mvc.ViewFeatures.IModelExpressionProvider ModelExpressionProvider { get; private set; } = default!;
#nullable disable
#nullable restore
[global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute]
public global::Microsoft.AspNetCore.Mvc.IUrlHelper Url { get; private set; } = default!;
#nullable disable
#nullable restore
[global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute]
public global::Microsoft.AspNetCore.Mvc.IViewComponentHelper Component { get; private set; } = default!;
#nullable disable
#nullable restore
[global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute]
public global::Microsoft.AspNetCore.Mvc.Rendering.IJsonHelper Json { get; private set; } = default!;
#nullable disable
#nullable restore
[global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute]
public global::Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper<UserData> Html { get; private set; } = default!;
#nullable disable
}
}
#pragma warning restore 1591

View File

@@ -0,0 +1,74 @@
// A test file that mimics the output of compiling a `.cshtml` file
// <auto-generated/>
#pragma warning disable 1591
[assembly: global::Microsoft.AspNetCore.Razor.Hosting.RazorCompiledItemAttribute(typeof(test.Views.Views_Test_Test1), @"mvc.1.0.view", @"/Views/Test/Test1.cshtml")]
namespace test.Views
{
#line hidden
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
#nullable restore
using test;
#line default
#line hidden
#nullable disable
[global::Microsoft.AspNetCore.Razor.Hosting.RazorCompiledItemMetadataAttribute("Identifier", "/Views/Test/Test1.cshtml")]
public class Views_Test_Test1 : global::Microsoft.AspNetCore.Mvc.Razor.RazorPage<UserData>
{
#pragma warning disable 1998
public async override global::System.Threading.Tasks.Task ExecuteAsync()
{
#line 6 "Views/Test/Test1.cshtml"
if (Model != null)
{
#line default
#line hidden
#nullable disable
WriteLiteral(" <h3>Hello \"");
#nullable restore
#line 8 "Views/Test/Test1.cshtml"
Write(Html.Raw(Model.Name));
#line default
#line hidden
#nullable disable
WriteLiteral("\"</h3>\n");
#nullable restore
#line 9 "Views/Test/Test1.cshtml"
}
#line default
#line hidden
#nullable disable
}
#pragma warning restore 1998
#nullable restore
[global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute]
public global::Microsoft.AspNetCore.Mvc.ViewFeatures.IModelExpressionProvider ModelExpressionProvider { get; private set; } = default!;
#nullable disable
#nullable restore
[global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute]
public global::Microsoft.AspNetCore.Mvc.IUrlHelper Url { get; private set; } = default!;
#nullable disable
#nullable restore
[global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute]
public global::Microsoft.AspNetCore.Mvc.IViewComponentHelper Component { get; private set; } = default!;
#nullable disable
#nullable restore
[global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute]
public global::Microsoft.AspNetCore.Mvc.Rendering.IJsonHelper Json { get; private set; } = default!;
#nullable disable
#nullable restore
[global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute]
public global::Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper<UserData> Html { get; private set; } = default!;
#nullable disable
}
}
#pragma warning restore 1591

View File

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

View File

@@ -0,0 +1,12 @@
edges
| Controllers/TestController.cs:13:41:13:47 | tainted : UserData | Controllers/TestController.cs:14:30:14:36 | access to parameter tainted : UserData |
| Controllers/TestController.cs:14:30:14:36 | access to parameter tainted : UserData | Views/Test/Test1.cshtml:8:16:8:20 | access to property Model : UserData |
| Views/Test/Test1.cshtml:8:16:8:20 | access to property Model : UserData | Views/Test/Test1.cshtml:8:16:8:25 | access to property Name |
nodes
| Controllers/TestController.cs:13:41:13:47 | tainted : UserData | semmle.label | tainted : UserData |
| Controllers/TestController.cs:14:30:14:36 | access to parameter tainted : UserData | semmle.label | access to parameter tainted : UserData |
| Views/Test/Test1.cshtml:8:16:8:20 | access to property Model : UserData | semmle.label | access to property Model : UserData |
| Views/Test/Test1.cshtml:8:16:8:25 | access to property Name | semmle.label | access to property Name |
subpaths
#select
| Views/Test/Test1.cshtml:8:16:8:25 | access to property Name | Controllers/TestController.cs:13:41:13:47 | tainted : UserData | Views/Test/Test1.cshtml:8:16:8:25 | 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:41:13:47 | tainted : UserData | User-provided value |

View File

@@ -0,0 +1 @@
Security Features/CWE-079/XSS.ql

View File

@@ -0,0 +1,46 @@
# A script for generating code from .cshtml files, mimicking the output of the C# compiler with an option that is not available from the codeql test runner.
import sys
import os
work_dir = os.path.dirname(sys.argv[0])
gen_dir = f"{work_dir}/Generated"
with open(f"{gen_dir}/Template.g") as f:
template = f.read()
def process_file(path: str):
"""
Generates the file from the .cshtml file at `path`.
`path` is a relative filepath from `work_dir`.
"""
# The location of the .cshtml file is the only relevant part for these tests; its contents are assumed to be the same.
assert path.endswith(".cshtml")
path = path.lstrip("/")
path_under = path.replace("/", "_")[:-len(".cshtml")]
gen = template.replace("$PATHSLASH", path).replace("$PATHUNDER", path_under)
with open(f"{gen_dir}/{path_under}.cshtml.g.cs", "w") as f:
f.write(gen)
def process_dir(path: str):
"""
Generates all the .cshtml files in the directory `path`.
`path` is a relative filepath from `work_dir`.
"""
abs_path = f"{work_dir}/{path}"
assert os.path.isdir(abs_path)
for sub in os.listdir(abs_path):
sub_abs = f"{abs_path}/{sub}"
sub_rel = f"{path}/{sub}"
if sub.endswith(".cshtml") and os.path.isfile(sub_abs):
process_file(sub_rel)
elif os.path.isdir(sub_abs) and ".testproj" not in sub_abs:
process_dir(sub_rel)
process_dir("")

View File

@@ -0,0 +1,3 @@
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:../../../../resources/stubs/_frameworks/Microsoft.AspNetCore.App/Microsoft.AspNetCore.App.csproj