diff --git a/csharp/ql/integration-tests/all-platforms/blazor/BlazorTest/Components/Pages/TestPage.razor b/csharp/ql/integration-tests/all-platforms/blazor/BlazorTest/Components/Pages/TestPage.razor
index 39238d724298..ac3ccbe19207 100644
--- a/csharp/ql/integration-tests/all-platforms/blazor/BlazorTest/Components/Pages/TestPage.razor
+++ b/csharp/ql/integration-tests/all-platforms/blazor/BlazorTest/Components/Pages/TestPage.razor
@@ -81,6 +81,10 @@
+
+
+
+
@code {
public class Container
diff --git a/csharp/ql/integration-tests/all-platforms/blazor/XSS.qlref b/csharp/ql/integration-tests/all-platforms/blazor/XSS.qlref
new file mode 100644
index 000000000000..faad1d6403c1
--- /dev/null
+++ b/csharp/ql/integration-tests/all-platforms/blazor/XSS.qlref
@@ -0,0 +1 @@
+Security Features/CWE-079/XSS.ql
\ No newline at end of file
diff --git a/csharp/ql/integration-tests/all-platforms/blazor_build_mode_none/BlazorTest/Components/Pages/TestPage.razor b/csharp/ql/integration-tests/all-platforms/blazor_build_mode_none/BlazorTest/Components/Pages/TestPage.razor
index 39238d724298..ac3ccbe19207 100644
--- a/csharp/ql/integration-tests/all-platforms/blazor_build_mode_none/BlazorTest/Components/Pages/TestPage.razor
+++ b/csharp/ql/integration-tests/all-platforms/blazor_build_mode_none/BlazorTest/Components/Pages/TestPage.razor
@@ -81,6 +81,10 @@
+
+
+
+
@code {
public class Container
diff --git a/csharp/ql/integration-tests/all-platforms/blazor_build_mode_none/XSS.qlref b/csharp/ql/integration-tests/all-platforms/blazor_build_mode_none/XSS.qlref
new file mode 100644
index 000000000000..faad1d6403c1
--- /dev/null
+++ b/csharp/ql/integration-tests/all-platforms/blazor_build_mode_none/XSS.qlref
@@ -0,0 +1 @@
+Security Features/CWE-079/XSS.ql
\ No newline at end of file
diff --git a/csharp/ql/integration-tests/all-platforms/blazor_net_8/BlazorTest/Components/Pages/TestPage.razor b/csharp/ql/integration-tests/all-platforms/blazor_net_8/BlazorTest/Components/Pages/TestPage.razor
index 39238d724298..ac3ccbe19207 100644
--- a/csharp/ql/integration-tests/all-platforms/blazor_net_8/BlazorTest/Components/Pages/TestPage.razor
+++ b/csharp/ql/integration-tests/all-platforms/blazor_net_8/BlazorTest/Components/Pages/TestPage.razor
@@ -81,6 +81,10 @@
+
+
+
+
@code {
public class Container
diff --git a/csharp/ql/integration-tests/all-platforms/blazor_net_8/XSS.qlref b/csharp/ql/integration-tests/all-platforms/blazor_net_8/XSS.qlref
new file mode 100644
index 000000000000..faad1d6403c1
--- /dev/null
+++ b/csharp/ql/integration-tests/all-platforms/blazor_net_8/XSS.qlref
@@ -0,0 +1 @@
+Security Features/CWE-079/XSS.ql
\ No newline at end of file
diff --git a/csharp/ql/lib/semmle/code/csharp/frameworks/microsoft/aspnetcore/Components.qll b/csharp/ql/lib/semmle/code/csharp/frameworks/microsoft/aspnetcore/Components.qll
index 6e37fc0480fb..d5782b26851a 100644
--- a/csharp/ql/lib/semmle/code/csharp/frameworks/microsoft/aspnetcore/Components.qll
+++ b/csharp/ql/lib/semmle/code/csharp/frameworks/microsoft/aspnetcore/Components.qll
@@ -112,6 +112,16 @@ class MicrosoftAspNetCoreComponentsComponent extends Class {
}
}
+/**
+ * The `Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder::AddComponentParameter` method.
+ */
+private class MicrosoftAspNetCoreComponentsAddComponentParameterMethod extends Method {
+ MicrosoftAspNetCoreComponentsAddComponentParameterMethod() {
+ this.hasFullyQualifiedName("Microsoft.AspNetCore.Components.Rendering", "RenderTreeBuilder",
+ "AddComponentParameter")
+ }
+}
+
private module Sources {
private import semmle.code.csharp.security.dataflow.flowsources.Remote
@@ -133,3 +143,45 @@ private module Sources {
override string getSourceType() { result = "ASP.NET Core component route parameter" }
}
}
+
+private module JumpNodes {
+ /**
+ * A call to `Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder::AddComponentParameter` which
+ * sets the value of a parameter.
+ */
+ private class ParameterPassingCall extends Call {
+ ParameterPassingCall() {
+ this.getTarget() instanceof MicrosoftAspNetCoreComponentsAddComponentParameterMethod
+ }
+
+ /**
+ * Gets the property whose value is being set.
+ */
+ Property getParameterProperty() {
+ result.getAnAttribute() instanceof MicrosoftAspNetCoreComponentsParameterAttribute and
+ exists(NameOfExpr ne | ne = this.getArgument(1) |
+ result.getAnAccess() = ne.getAccess().(MemberAccess)
+ )
+ }
+
+ /**
+ * Gets the value being set.
+ */
+ Expr getParameterValue() { result = this.getArgument(2) }
+ }
+
+ private class ComponentParameterJump extends DataFlow::NonLocalJumpNode {
+ ParameterPassingCall call;
+ Property prop;
+
+ ComponentParameterJump() {
+ prop = call.getParameterProperty() and
+ this.asExpr() = call.getParameterValue()
+ }
+
+ override DataFlow::Node getAJumpSuccessor(boolean preservesValue) {
+ preservesValue = true and
+ result.asExpr() = prop.getAnAccess()
+ }
+ }
+}
diff --git a/csharp/ql/test/library-tests/frameworks/microsoft/aspnetcore/blazor/Name.cs b/csharp/ql/test/library-tests/frameworks/microsoft/aspnetcore/blazor/Name.cs
new file mode 100644
index 000000000000..a9d098470e44
--- /dev/null
+++ b/csharp/ql/test/library-tests/frameworks/microsoft/aspnetcore/blazor/Name.cs
@@ -0,0 +1,22 @@
+namespace VulnerableBlazorApp.Components
+{
+ using Microsoft.AspNetCore.Components;
+
+ public partial class Name : Microsoft.AspNetCore.Components.ComponentBase
+ {
+ protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder)
+ {
+ if (TheName is not null)
+ {
+ builder.OpenElement(0, "div");
+ builder.OpenElement(1, "p");
+ builder.AddContent(2, (MarkupString)TheName);
+ builder.CloseElement();
+ builder.CloseElement();
+ }
+ }
+
+ [Parameter]
+ public string TheName { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/csharp/ql/test/library-tests/frameworks/microsoft/aspnetcore/blazor/NameList.cs b/csharp/ql/test/library-tests/frameworks/microsoft/aspnetcore/blazor/NameList.cs
new file mode 100644
index 000000000000..ceffb35303e5
--- /dev/null
+++ b/csharp/ql/test/library-tests/frameworks/microsoft/aspnetcore/blazor/NameList.cs
@@ -0,0 +1,50 @@
+namespace VulnerableBlazorApp.Components
+{
+ using System.Collections.Generic;
+ using Microsoft.AspNetCore.Components;
+
+ [RouteAttribute("/names/{name?}")]
+ public partial class NameList : Microsoft.AspNetCore.Components.ComponentBase
+ {
+ protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder)
+ {
+ if (Names is not null)
+ {
+ builder.OpenElement(0, "div");
+ builder.OpenElement(1, "ul");
+ foreach (var name in Names)
+ {
+ builder.OpenElement(2, "li");
+ builder.OpenComponent(3);
+ builder.AddComponentParameter(4, nameof(VulnerableBlazorApp.Components.Name.TheName), name);
+ builder.CloseComponent();
+ builder.CloseElement();
+ }
+ builder.CloseElement();
+ builder.CloseElement();
+ }
+
+ builder.OpenElement(5, "div");
+ builder.OpenElement(6, "p");
+ builder.AddContent(7, "Name: ");
+ builder.OpenComponent(8);
+ builder.AddComponentParameter(9, nameof(VulnerableBlazorApp.Components.Name.TheName), Name);
+ builder.CloseComponent();
+ builder.CloseElement();
+ }
+
+ [Parameter]
+ public string Name { get; set; }
+
+ protected override void OnParametersSet()
+ {
+ if (Name is not null)
+ {
+ Names.Add(Name);
+ }
+ }
+
+
+ public List Names { get; set; } = new List();
+ }
+}
\ No newline at end of file
diff --git a/csharp/ql/test/library-tests/frameworks/microsoft/aspnetcore/blazor/Xss.expected b/csharp/ql/test/library-tests/frameworks/microsoft/aspnetcore/blazor/Xss.expected
new file mode 100644
index 000000000000..951269f2b580
--- /dev/null
+++ b/csharp/ql/test/library-tests/frameworks/microsoft/aspnetcore/blazor/Xss.expected
@@ -0,0 +1,12 @@
+edges
+| NameList.cs:31:99:31:102 | access to property Name : String | Name.cs:13:53:13:59 | access to property TheName | provenance | Sink:MaD:149 |
+nodes
+| Components_Pages_TestPage_razor.g.cs:138:15:138:22 | access to property UrlParam | semmle.label | access to property UrlParam |
+| Components_Pages_TestPage_razor.g.cs:188:18:188:27 | access to property QueryParam | semmle.label | access to property QueryParam |
+| Name.cs:13:53:13:59 | access to property TheName | semmle.label | access to property TheName |
+| NameList.cs:31:99:31:102 | access to property Name : String | semmle.label | access to property Name : String |
+subpaths
+#select
+| Components_Pages_TestPage_razor.g.cs:138:15:138:22 | access to property UrlParam | Components_Pages_TestPage_razor.g.cs:138:15:138:22 | access to property UrlParam | Components_Pages_TestPage_razor.g.cs:138:15:138:22 | access to property UrlParam | $@ flows to here and is written to HTML or JavaScript. | Components_Pages_TestPage_razor.g.cs:138:15:138:22 | access to property UrlParam | User-provided value |
+| Components_Pages_TestPage_razor.g.cs:188:18:188:27 | access to property QueryParam | Components_Pages_TestPage_razor.g.cs:188:18:188:27 | access to property QueryParam | Components_Pages_TestPage_razor.g.cs:188:18:188:27 | access to property QueryParam | $@ flows to here and is written to HTML or JavaScript. | Components_Pages_TestPage_razor.g.cs:188:18:188:27 | access to property QueryParam | User-provided value |
+| Name.cs:13:53:13:59 | access to property TheName | NameList.cs:31:99:31:102 | access to property Name : String | Name.cs:13:53:13:59 | access to property TheName | $@ flows to here and is written to HTML or JavaScript. | NameList.cs:31:99:31:102 | access to property Name : String | User-provided value |
diff --git a/csharp/ql/test/library-tests/frameworks/microsoft/aspnetcore/blazor/Xss.qlref b/csharp/ql/test/library-tests/frameworks/microsoft/aspnetcore/blazor/Xss.qlref
new file mode 100644
index 000000000000..faad1d6403c1
--- /dev/null
+++ b/csharp/ql/test/library-tests/frameworks/microsoft/aspnetcore/blazor/Xss.qlref
@@ -0,0 +1 @@
+Security Features/CWE-079/XSS.ql
\ No newline at end of file
diff --git a/csharp/ql/test/library-tests/frameworks/microsoft/aspnetcore/blazor/remoteFlowSource.expected b/csharp/ql/test/library-tests/frameworks/microsoft/aspnetcore/blazor/remoteFlowSource.expected
index 2c845e8e4001..2a9268cf01e3 100644
--- a/csharp/ql/test/library-tests/frameworks/microsoft/aspnetcore/blazor/remoteFlowSource.expected
+++ b/csharp/ql/test/library-tests/frameworks/microsoft/aspnetcore/blazor/remoteFlowSource.expected
@@ -2,3 +2,6 @@
| Components_Pages_TestPage_razor.g.cs:138:15:138:22 | access to property UrlParam | ASP.NET Core component route parameter |
| Components_Pages_TestPage_razor.g.cs:176:1:176:10 | access to property QueryParam | external |
| Components_Pages_TestPage_razor.g.cs:188:18:188:27 | access to property QueryParam | external |
+| NameList.cs:31:99:31:102 | access to property Name | ASP.NET Core component route parameter |
+| NameList.cs:41:17:41:20 | access to property Name | ASP.NET Core component route parameter |
+| NameList.cs:43:27:43:30 | access to property Name | ASP.NET Core component route parameter |