mirror of
https://github.com/github/codeql.git
synced 2026-04-23 07:45:17 +02:00
Unit tests - Write script to aid generating necessary code from .cshtml files.
This commit is contained in:
@@ -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() }
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,9 @@
|
||||
@namespace test
|
||||
@model UserData
|
||||
@{
|
||||
}
|
||||
|
||||
@if (Model != null)
|
||||
{
|
||||
<h3>Hello "@Html.Raw(Model.Name)"</h3>
|
||||
}
|
||||
@@ -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 |
|
||||
@@ -0,0 +1 @@
|
||||
Security Features/CWE-079/XSS.ql
|
||||
@@ -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("")
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user