Skip to content

Commit ac3f642

Browse files
Unit tests - Write script to aid generating necessary code from .cshtml files.
1 parent 12a579e commit ac3f642

File tree

9 files changed

+275
-16
lines changed

9 files changed

+275
-16
lines changed

csharp/ql/lib/semmle/code/csharp/security/dataflow/XSSFlowSteps.qll

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
1+
/** Definitions for additional flow steps for cross-site scripting (XSS) vulnerabilities. */
2+
13
import csharp
24
private import codeql.util.Unit
35
private import semmle.code.csharp.frameworks.microsoft.AspNetCore
46

5-
/** An additional flow step for cross-site scripting (XSS) vulnerabilities */
7+
/**
8+
* A unit class for providing additional flow steps for cross-site scripting (XSS) vulnerabilities.
9+
* Extend to provide additional flow steps.
10+
*/
611
class XssAdditionalFlowStep extends Unit {
12+
/** Holds if there is an additional dataflow step from `node1` to `node2`. */
713
abstract predicate isAdditionalFlowStep(DataFlow::Node node1, DataFlow::Node node2);
814
}
915

@@ -34,6 +40,17 @@ private class ViewCall extends MethodCall {
3440
result = this.getController().getAnActionMethod()
3541
}
3642

43+
/**
44+
* Gets the action name that this call refers to, if any.
45+
* This is either the name argument, or the name of the action method calling this if there is no name argument.
46+
*/
47+
string getActionName() {
48+
result = this.getNameArgument()
49+
or
50+
not exists(this.getNameArgument()) and
51+
result = this.getActionMethod().getName()
52+
}
53+
3754
/** Gets the MVC controller that this call is made from, if any. */
3855
MicrosoftAspNetCoreMvcController getController() {
3956
result = this.getEnclosingCallable().getDeclaringType()
@@ -91,31 +108,38 @@ private predicate viewCallRefersToPageRelative(ViewCall vc, RazorPage rp) {
91108
)
92109
}
93110

111+
/** Gets the `i`th template for view discovery. */
112+
private string getViewSearchTemplate(int i) {
113+
i = 0 and result = "/Views/{1}/{0}.cshtml"
114+
or
115+
i = 1 and result = "/Views/Shared/{0}.cshtml"
116+
}
117+
118+
/** A filepath that should be searched for a View call. */
94119
private class RelativeViewCallFilepath extends NormalizableFilepath {
95-
ViewCall vc;
96-
int idx;
120+
ViewCall vc_;
121+
int idx_;
97122

98123
RelativeViewCallFilepath() {
99-
exists(string actionName |
100-
actionName = vc.getNameArgument() and
101-
not actionName.matches("%.cshtml")
102-
or
103-
not exists(vc.getNameArgument()) and
104-
actionName = vc.getActionMethod().getName()
105-
|
106-
idx = 0 and
107-
this = "/Views/" + vc.getControllerName() + "/" + actionName + ".cshtml"
124+
exists(string template | template = getViewSearchTemplate(idx_) |
125+
this =
126+
template.replaceAll("{0}", vc_.getActionName()).replaceAll("{1}", vc_.getControllerName())
108127
or
109-
idx = 1 and
110-
this = "/Views/Shared/" + actionName + ".cshtml"
128+
not exists(vc_.getControllerName()) and
129+
not template.matches("%{1}%") and
130+
this = template.replaceAll("{0}", vc_.getActionName())
111131
)
112132
}
113133

114-
predicate hasViewCallWithIndex(ViewCall vc2, int idx2) { vc = vc2 and idx = idx2 }
134+
/** Holds if this string is the `idx`th path that will be searched for the `vc` call. */
135+
predicate hasViewCallWithIndex(ViewCall vc, int idx) { vc = vc_ and idx = idx_ }
115136
}
116137

117138
// TODO: this could be a shared library
118-
/** A filepath that should be normalized. */
139+
/**
140+
* A filepath that should be normalized.
141+
* Extend to provide additional strings that should be normalized as filepaths.
142+
*/
119143
abstract private class NormalizableFilepath extends string {
120144
bindingset[this]
121145
NormalizableFilepath() { any() }
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
namespace test;
2+
3+
using System.Net;
4+
using Microsoft.AspNetCore.Mvc;
5+
using Microsoft.AspNetCore.Mvc.RazorPages;
6+
7+
public class UserData
8+
{
9+
public string Name { get; set; }
10+
}
11+
12+
public class TestController : Controller {
13+
public IActionResult test1(UserData tainted) {
14+
return View("Test1", tainted);
15+
}
16+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// A test file that mimics the output of compiling a `.cshtml` file
2+
// <auto-generated/>
3+
#pragma warning disable 1591
4+
[assembly: global::Microsoft.AspNetCore.Razor.Hosting.RazorCompiledItemAttribute(typeof(test.Views.$PATHUNDER), @"mvc.1.0.view", @"/$PATHSLASH")]
5+
namespace test.Views
6+
{
7+
#line hidden
8+
using System;
9+
using System.Collections.Generic;
10+
using System.Linq;
11+
using System.Threading.Tasks;
12+
using Microsoft.AspNetCore.Mvc;
13+
using Microsoft.AspNetCore.Mvc.Rendering;
14+
using Microsoft.AspNetCore.Mvc.ViewFeatures;
15+
#nullable restore
16+
using test;
17+
18+
#line default
19+
#line hidden
20+
#nullable disable
21+
[global::Microsoft.AspNetCore.Razor.Hosting.RazorCompiledItemMetadataAttribute("Identifier", "/$PATHSLASH")]
22+
public class $PATHUNDER : global::Microsoft.AspNetCore.Mvc.Razor.RazorPage<UserData>
23+
{
24+
#pragma warning disable 1998
25+
public async override global::System.Threading.Tasks.Task ExecuteAsync()
26+
{
27+
#line 6 "$PATHSLASH"
28+
if (Model != null)
29+
{
30+
31+
#line default
32+
#line hidden
33+
#nullable disable
34+
WriteLiteral(" <h3>Hello \"");
35+
#nullable restore
36+
#line 8 "$PATHSLASH"
37+
Write(Html.Raw(Model.Name));
38+
39+
#line default
40+
#line hidden
41+
#nullable disable
42+
WriteLiteral("\"</h3>\n");
43+
#nullable restore
44+
#line 9 "$PATHSLASH"
45+
}
46+
47+
#line default
48+
#line hidden
49+
#nullable disable
50+
}
51+
#pragma warning restore 1998
52+
#nullable restore
53+
[global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute]
54+
public global::Microsoft.AspNetCore.Mvc.ViewFeatures.IModelExpressionProvider ModelExpressionProvider { get; private set; } = default!;
55+
#nullable disable
56+
#nullable restore
57+
[global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute]
58+
public global::Microsoft.AspNetCore.Mvc.IUrlHelper Url { get; private set; } = default!;
59+
#nullable disable
60+
#nullable restore
61+
[global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute]
62+
public global::Microsoft.AspNetCore.Mvc.IViewComponentHelper Component { get; private set; } = default!;
63+
#nullable disable
64+
#nullable restore
65+
[global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute]
66+
public global::Microsoft.AspNetCore.Mvc.Rendering.IJsonHelper Json { get; private set; } = default!;
67+
#nullable disable
68+
#nullable restore
69+
[global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute]
70+
public global::Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper<UserData> Html { get; private set; } = default!;
71+
#nullable disable
72+
}
73+
}
74+
#pragma warning restore 1591
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// A test file that mimics the output of compiling a `.cshtml` file
2+
// <auto-generated/>
3+
#pragma warning disable 1591
4+
[assembly: global::Microsoft.AspNetCore.Razor.Hosting.RazorCompiledItemAttribute(typeof(test.Views.Views_Test_Test1), @"mvc.1.0.view", @"/Views/Test/Test1.cshtml")]
5+
namespace test.Views
6+
{
7+
#line hidden
8+
using System;
9+
using System.Collections.Generic;
10+
using System.Linq;
11+
using System.Threading.Tasks;
12+
using Microsoft.AspNetCore.Mvc;
13+
using Microsoft.AspNetCore.Mvc.Rendering;
14+
using Microsoft.AspNetCore.Mvc.ViewFeatures;
15+
#nullable restore
16+
using test;
17+
18+
#line default
19+
#line hidden
20+
#nullable disable
21+
[global::Microsoft.AspNetCore.Razor.Hosting.RazorCompiledItemMetadataAttribute("Identifier", "/Views/Test/Test1.cshtml")]
22+
public class Views_Test_Test1 : global::Microsoft.AspNetCore.Mvc.Razor.RazorPage<UserData>
23+
{
24+
#pragma warning disable 1998
25+
public async override global::System.Threading.Tasks.Task ExecuteAsync()
26+
{
27+
#line 6 "Views/Test/Test1.cshtml"
28+
if (Model != null)
29+
{
30+
31+
#line default
32+
#line hidden
33+
#nullable disable
34+
WriteLiteral(" <h3>Hello \"");
35+
#nullable restore
36+
#line 8 "Views/Test/Test1.cshtml"
37+
Write(Html.Raw(Model.Name));
38+
39+
#line default
40+
#line hidden
41+
#nullable disable
42+
WriteLiteral("\"</h3>\n");
43+
#nullable restore
44+
#line 9 "Views/Test/Test1.cshtml"
45+
}
46+
47+
#line default
48+
#line hidden
49+
#nullable disable
50+
}
51+
#pragma warning restore 1998
52+
#nullable restore
53+
[global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute]
54+
public global::Microsoft.AspNetCore.Mvc.ViewFeatures.IModelExpressionProvider ModelExpressionProvider { get; private set; } = default!;
55+
#nullable disable
56+
#nullable restore
57+
[global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute]
58+
public global::Microsoft.AspNetCore.Mvc.IUrlHelper Url { get; private set; } = default!;
59+
#nullable disable
60+
#nullable restore
61+
[global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute]
62+
public global::Microsoft.AspNetCore.Mvc.IViewComponentHelper Component { get; private set; } = default!;
63+
#nullable disable
64+
#nullable restore
65+
[global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute]
66+
public global::Microsoft.AspNetCore.Mvc.Rendering.IJsonHelper Json { get; private set; } = default!;
67+
#nullable disable
68+
#nullable restore
69+
[global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute]
70+
public global::Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper<UserData> Html { get; private set; } = default!;
71+
#nullable disable
72+
}
73+
}
74+
#pragma warning restore 1591
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
@namespace test
2+
@model UserData
3+
@{
4+
}
5+
6+
@if (Model != null)
7+
{
8+
<h3>Hello "@Html.Raw(Model.Name)"</h3>
9+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
edges
2+
| Controllers/TestController.cs:13:41:13:47 | tainted : UserData | Controllers/TestController.cs:14:30:14:36 | access to parameter tainted : UserData |
3+
| 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 |
4+
| 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 |
5+
nodes
6+
| Controllers/TestController.cs:13:41:13:47 | tainted : UserData | semmle.label | tainted : UserData |
7+
| Controllers/TestController.cs:14:30:14:36 | access to parameter tainted : UserData | semmle.label | access to parameter tainted : UserData |
8+
| Views/Test/Test1.cshtml:8:16:8:20 | access to property Model : UserData | semmle.label | access to property Model : UserData |
9+
| Views/Test/Test1.cshtml:8:16:8:25 | access to property Name | semmle.label | access to property Name |
10+
subpaths
11+
#select
12+
| 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 |
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Security Features/CWE-079/XSS.ql
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# 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.
2+
3+
import sys
4+
import os
5+
6+
work_dir = os.path.dirname(sys.argv[0])
7+
gen_dir = f"{work_dir}/Generated"
8+
with open(f"{gen_dir}/Template.g") as f:
9+
template = f.read()
10+
11+
12+
def process_file(path: str):
13+
"""
14+
Generates the file from the .cshtml file at `path`.
15+
`path` is a relative filepath from `work_dir`.
16+
"""
17+
# The location of the .cshtml file is the only relevant part for these tests; its contents are assumed to be the same.
18+
assert path.endswith(".cshtml")
19+
path = path.lstrip("/")
20+
path_under = path.replace("/", "_")[:-len(".cshtml")]
21+
22+
gen = template.replace("$PATHSLASH", path).replace("$PATHUNDER", path_under)
23+
24+
with open(f"{gen_dir}/{path_under}.cshtml.g.cs", "w") as f:
25+
f.write(gen)
26+
27+
28+
def process_dir(path: str):
29+
"""
30+
Generates all the .cshtml files in the directory `path`.
31+
`path` is a relative filepath from `work_dir`.
32+
"""
33+
abs_path = f"{work_dir}/{path}"
34+
assert os.path.isdir(abs_path)
35+
36+
for sub in os.listdir(abs_path):
37+
sub_abs = f"{abs_path}/{sub}"
38+
sub_rel = f"{path}/{sub}"
39+
40+
if sub.endswith(".cshtml") and os.path.isfile(sub_abs):
41+
process_file(sub_rel)
42+
elif os.path.isdir(sub_abs) and ".testproj" not in sub_abs:
43+
process_dir(sub_rel)
44+
45+
46+
process_dir("")
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
semmle-extractor-options: /nostdlib /noconfig
2+
semmle-extractor-options: --load-sources-from-project:${testdir}/../../../../resources/stubs/_frameworks/Microsoft.NETCore.App/Microsoft.NETCore.App.csproj
3+
semmle-extractor-options: --load-sources-from-project:../../../../resources/stubs/_frameworks/Microsoft.AspNetCore.App/Microsoft.AspNetCore.App.csproj

0 commit comments

Comments
 (0)