CWE-80: Improper Neutralization of Script-Related HTML Tags in a Web Page (Basic XSS) - C# / ASP.NET
Overview
CWE-80 is the tag-injection case of XSS: <, > and & reach the rendered
page without being converted to entities, so an attacker's <script> or
<img onerror=...> is parsed as markup rather than shown as text.
The fix is the output encoding described on
CWE-79 - C#, which is where the encoders live:
Razor's automatic escaping, HtmlEncoder, JavaScriptEncoder, UrlEncoder,
the classic HttpUtility/WebUtility equivalents, and Ganss.Xss
HtmlSanitizer for content that must keep real markup. This page does not repeat
them.
What belongs here is the failure mode that produces CWE-80 findings specifically - trying to remove the dangerous tags instead of encoding the characters that make them tags.
Common Vulnerable Patterns
Stripping tags with a blocklist
// VULNERABLE - removing what looks dangerous, rather than encoding
public static string Clean(string input)
{
var output = Regex.Replace(input, "<script>",
string.Empty, RegexOptions.IgnoreCase);
output = output.Replace("javascript:", string.Empty);
return output;
}
// Rendered with @Html.Raw(Clean(model.Comment)) - encoding is now switched off
Why this is vulnerable: every one of these survives it.
<img src=x onerror=alert(1)> no <script> tag at all
<svg onload=alert(1)> same, different element
<scr<script>ipt>alert(1)</script> the inner match is removed, leaving <script>
javascript:alert(1) the literal "javascript:" never appears
The set of ways to execute script is open-ended and grows with the HTML and
browser specifications, so a blocklist is guessing against a list it cannot
enumerate. The second replacement is worse than useless: removing
javascript: once turns javajavascript:script: into javascript:.
Re-trusting the filtered value
Why this is vulnerable: @Html.Raw() and returning IHtmlContent or
HtmlString do not sanitize. They tell Razor the value is trusted markup and
switch off the automatic encoding that was already handling it correctly.
Applying either to a filtered user value hands the parser whatever the filter
missed - so the blocklist above is only reachable as a vulnerability because
something turned the encoding off.
Secure Patterns
Encode, and leave Razor alone
@Model.Comment is already correct for element content. The worked examples,
including the other output contexts, are on
CWE-79 - C#.
Encoding is what makes the tag-injection case safe: once < is < there is
no tag left to blocklist, whatever the attacker writes.
When the input really is HTML
If users are meant to submit formatting, encoding it would defeat the feature. Use an allowlist sanitizer, which parses the HTML and rebuilds it from the elements you permit rather than deleting what looks bad:
// SECURE - allowlist parse-and-rebuild, not a blocklist filter
using Ganss.Xss;
var sanitizer = new HtmlSanitizer();
sanitizer.AllowedTags.Clear();
sanitizer.AllowedTags.UnionWith(new[] { "p", "br", "strong", "em", "ul", "ol", "li", "a", "code" });
sanitizer.AllowedAttributes.Clear();
sanitizer.AllowedAttributes.UnionWith(new[] { "href", "title" });
sanitizer.AllowedSchemes.Clear();
sanitizer.AllowedSchemes.UnionWith(new[] { "http", "https", "mailto" });
var safeHtml = sanitizer.Sanitize(dirtyHtml);
Why this works: the sanitizer parses the input into a document and
re-serialises it from an allowlist, so anything not explicitly permitted cannot
survive. The open-ended set of ways to run script stops mattering, because the
question is no longer "is this dangerous" but "is this on the list". Clearing
AllowedSchemes first closes javascript: in an href, which a tag allowlist
alone would still let through.
Install with dotnet add package HtmlSanitizer --version 9.2.1039 or later -
that release closes a SanitizeDom(string) wrapper-element attribute bypass.
The fix carries no advisory of its own, so an advisory check alone will pass an
older, affected version. Configuration options and the Razor integration are
covered on
CWE-79 - C#.
Considerations
Whether the finding is CWE-80 or CWE-79 rarely changes the fix. Both are resolved by encoding for the context the value lands in. The label tells you what the scanner matched - tag characters in element content - not the shape of the remediation.
It does change where to look next. CWE-80 only describes element content.
An application that failed to encode there has usually failed in the contexts
CWE-80 does not name: quoted and unquoted attributes, href and src values,
inline <script> blocks, and Blazor or JavaScript DOM sinks. Those are CWE-79's
territory, and one CWE-80 instance is a reason to check them.
A filter kept for other reasons is not the control. Teams sometimes strip
<script> for content-policy or display reasons and then treat it as the
security fix. If the filter stays, encoding still has to be what protects the
page.
Additional Resources
- CWE-80: Improper Neutralization of Script-Related HTML Tags
- CWE-79 - C# - the encoders, framework escaping and sanitizer configuration this page defers to
- HtmlSanitizer (Ganss.Xss)
- OWASP XSS Prevention Cheat Sheet