Review HTML-encoding bypasses
Matches methods that bypass the HTML encoder that Razor and Blazor apply by default: Html.Raw(), new HtmlString(), new MarkupString(), MvcHtmlString.Create(), and the AppendHtml(string) / SetHtmlContent(string) members of tag helpers and IHtmlContentBuilder.
Each of these members tells the view engine trust this string, it is already HTML. When the string contains data that a user controls (a comment, a profile field, a query string value), a payload such as <script> is rendered as markup and executed in the browser of every visitor: cross-site scripting. This is CWE-79 (OWASP Top 10 A03 Injection).
Compiled Razor views are generated methods and are deliberately included: the issue then points to the view. Methods that also assemble the string (String.Concat(), String.Format(), interpolation, StringBuilder) are reported with the High severity, since the raw fragment is built rather than picked from a trusted source.
Flagged Code
Each of these members tells the view engine trust this string, it is already HTML. When the string contains data that a user controls (a comment, a profile field, a query string value), a payload such as <script> is rendered as markup and executed in the browser of every visitor: cross-site scripting. This is CWE-79 (OWASP Top 10 A03 Injection).
Compiled Razor views are generated methods and are deliberately included: the issue then points to the view. Methods that also assemble the string (String.Concat(), String.Format(), interpolation, StringBuilder) are reported with the High severity, since the raw fragment is built rather than picked from a trusted source.
Flagged Code
public class CommentViewComponent : ViewComponent {
public IViewComponentResult Invoke(Comment comment) {
var html = new HtmlString("<p>" + comment.Body + "</p>"); // flagged: raw HTML
return View(html);
}
}
Fixed Codepublic class CommentViewComponent : ViewComponent {
public IViewComponentResult Invoke(Comment comment) {
return View(comment); // the view writes @Model.Body, encoded by Razor
}
}How to fix
Let the view engine encode the value: write @Model.Body instead of @Html.Raw(Model.Body). When markup must be produced from data, build it with an IHtmlContentBuilder and Append() (encoded) the dynamic parts, or sanitize the fragment with an allow-list HTML sanitizer before wrapping it in an HtmlString.
Suppress the issue when the raw string is a constant or comes from a trusted source, such as a resource file or markup authored by administrators only.
Suppress the issue when the raw string is a constant or comes from a trusted source, such as a resource file or markup authored by administrators only.
CQLinq source
This rule is written in CQLinq, C# LINQ over the NDepend code model. It ships with every new NDepend project: you can read it, adapt it, or use it as a starting point for your own custom rules.
// <Name>Review HTML-encoding bypasses</Name>
// <Id>ND3133:ReviewHtmlEncodingBypasses</Id>
warnif count > 0
// Third-party members are part of the code model only when the application uses them.
// Razor and Blazor HTML-encode every value by default; these members are the explicit opt-outs.
let rawHtmlMembers = ThirdParty.Methods.Where(m =>
// @Html.Raw(value) in MVC and Razor Pages, both ASP.NET Core and ASP.NET MVC 5
(m.SimpleName == "Raw" &&
m.ParentType.SimpleNameEqualsAny("IHtmlHelper", "IHtmlHelper<TModel>", "HtmlHelper", "HtmlHelper<TModel>") &&
m.ParentType.FullName.StartsWithAny("Microsoft.AspNetCore.Mvc.", "System.Web.Mvc.")) ||
// new HtmlString(value), new MarkupString(value), MvcHtmlString.Create(value)
(m.NbParameters == 1 && (m.IsConstructor || m.SimpleName == "Create") &&
m.ParentType.FullNameEqualsAny(
"Microsoft.AspNetCore.Html.HtmlString",
"Microsoft.AspNetCore.Components.MarkupString",
"System.Web.HtmlString",
"System.Web.Mvc.MvcHtmlString")) ||
// Tag helpers and IHtmlContentBuilder: AppendHtml(string) / SetHtmlContent(string) skip the encoder
(m.SimpleNameEqualsAny("AppendHtml", "SetHtmlContent") &&
m.Name.Contains("String") &&
m.ParentType.FullName.StartsWithAny("Microsoft.AspNetCore.Html.", "Microsoft.AspNetCore.Razor.TagHelpers.")))
.ToHashSetEx()
where rawHtmlMembers.Any()
// Compiled Razor views are generated methods: they are the primary place where Html.Raw() is used,
// hence generated methods are NOT excluded here.
from m in Application.Methods.UsingAny(rawHtmlMembers)
// The static constructor of a compiled Razor view only builds the tag helper attribute values
// written literally in the markup: new HtmlString("stylesheet") wraps a constant, never user input.
where !(m.IsClassConstructor &&
m.ParentType.HasAttribute("Microsoft.AspNetCore.Razor.Hosting.RazorCompiledItemMetadataAttribute".AllowNoMatch()) &&
m.MethodsCalled.Any(mc => mc.IsConstructor &&
mc.ParentType.FullName == "Microsoft.AspNetCore.Razor.TagHelpers.TagHelperAttribute"))
let rawHtmlMembersUsed = m.MethodsCalled.Intersect(rawHtmlMembers).ToArray()
// A raw fragment assembled with a string operation is the riskiest shape.
let composesString = m.MethodsCalled.Any(mc =>
(mc.ParentType.FullName == "System.String" && mc.SimpleNameEqualsAny("Concat", "Format", "Join", "Replace", "Insert")) ||
mc.ParentType.FullNameEqualsAny("System.Text.StringBuilder", "System.Runtime.CompilerServices.DefaultInterpolatedStringHandler"))
select new {
m,
rawHtmlMembersUsed,
composesString,
Debt = 15.ToMinutes().ToDebt(),
Severity = composesString ? Severity.High : Severity.Medium
}
//<Expl>
// The method *{0}* calls *{1}*, which emits a string as HTML without
// encoding it. If any part of that string derives from user input, a
// script injected in it runs in the browser of every visitor (XSS).
//</Expl>
// <Desc>
// Matches methods that bypass the HTML encoder that Razor and Blazor
// apply by default: *Html.Raw()*, *new HtmlString()*,
// *new MarkupString()*, *MvcHtmlString.Create()*, and the
// *AppendHtml(string)* / *SetHtmlContent(string)* members of tag helpers
// and *IHtmlContentBuilder*.
//
// Each of these members tells the view engine *trust this string, it is
// already HTML*. When the string contains data that a user controls (a
// comment, a profile field, a query string value), a payload such as
// *<script>* is rendered as markup and executed in the browser
// of every visitor: **cross-site scripting**. This is CWE-79 (OWASP Top
// 10 A03 Injection).
//
// Compiled Razor views are generated methods and are deliberately
// included: the issue then points to the view. Methods that also
// assemble the string (*String.Concat()*, *String.Format()*,
// interpolation, *StringBuilder*) are reported with the *High* severity,
// since the raw fragment is built rather than picked from a trusted
// source.
//
// **Flagged Code**
// ```csharp
// public class CommentViewComponent : ViewComponent {
// public IViewComponentResult Invoke(Comment comment) {
// var html = new HtmlString("<p>" + comment.Body + "</p>"); // flagged: raw HTML
// return View(html);
// }
// }
// ```
//
// **Fixed Code**
// ```csharp
// public class CommentViewComponent : ViewComponent {
// public IViewComponentResult Invoke(Comment comment) {
// return View(comment); // the view writes @Model.Body, encoded by Razor
// }
// }
// ```
// </Desc>
// <HowToFix>
// Let the view engine encode the value: write *@Model.Body* instead of
// *@Html.Raw(Model.Body)*. When markup must be produced from data, build
// it with an *IHtmlContentBuilder* and *Append()* (encoded) the dynamic
// parts, or sanitize the fragment with an allow-list HTML sanitizer
// before wrapping it in an *HtmlString*.
//
// Suppress the issue when the raw string is a constant or comes from a
// trusted source, such as a resource file or markup authored by
// administrators only.
// </HowToFix>
Open in the interactive Rules Explorer · All default rules
Security: all 39 items
- Software Composition Analysis (SCA) ND3120
- Transitive Software Composition Analysis (SCA) ND3121
- Don't hard-code secrets in constants ND3114
- Review credentials built from hard-coded values ND3115
- Don't use insecure deserializers ND3110
- Don't use weak hash algorithms (MD5, SHA1) ND3109
- Don't use DES/3DES/RC2 weak cipher algorithms ND3102
- Don't use weak password-based key derivation ND3123
- Don't use the ECB encryption mode ND3132
- Don't disable certificate validation ND3103
- Don't hard-code TLS/SSL protocol versions ND3122
- Review commands vulnerable to SQL injection ND3107
- Review data adapters vulnerable to SQL injection ND3108
- Review raw SQL built from composed strings ND3128
- Review LDAP filters composed from strings ND3137
- Don't enable external XML entity resolution (XXE) ND3111
- Review processes started with a composed arguments string ND3124
- Review web endpoints building file paths from their parameters ND3129
- Review custom archive extraction (Zip Slip) ND3135
- Review assemblies and types loaded from runtime-provided names ND3125
- Review web endpoints without [Authorize] nor [AllowAnonymous] ND3118
- Review state-changing endpoints opened to anonymous callers ND3130
- Review state-changing MVC actions without anti-forgery validation ND3119
- Review HTML-encoding bypasses ND3133
- Review explicitly disabled request validation ND3134
- Review redirects to a URL that is not validated as local ND3126
- Review request-influenced outbound HTTP calls (SSRF) ND3136
- Review insecure ASP.NET Core CORS policies and unguarded developer exception page ND3116
- Review cookies created without HttpOnly and Secure flags ND3117
- Review regular expressions without a match timeout (ReDoS) ND3112
- Review secrets compared with non-constant-time equality ND3113
- Review secrets passed to logging methods ND3127
- Review security operations that log nothing ND3131
- Don't use System.Random for security purposes ND3101
- Don't use CoSetProxyBlanket and CoInitializeSecurity ND3100
- Review publicly visible event handlers ND3104
- Pointers should not be publicly visible ND3105
- Seal methods that satisfy non-public interfaces ND3106
- Restrict sensitive APIs to reviewed components ND3138