Skip to content

C#: Fix cs/web/xss false positive on Razor tag-helper attribute values - #22628

Open
felickz wants to merge 3 commits into
github:mainfrom
forks-felickz:felickz-csharp-razor-tag-helper-xss-fp
Open

felickz wants to merge 3 commits into
github:mainfrom
forks-felickz:felickz-csharp-razor-tag-helper-xss-fp

Conversation

@felickz

@felickz felickz commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Summary

cs/web/xss currently flags RazorPageBase.WriteLiteral(...) calls that the Razor source generator emits for the value of an HTML attribute on an element that also carries a tag helper (for example, asp-for="Model.Something"). This is a false positive: the Razor codegen brackets these calls between BeginWriteTagHelperAttribute() / EndWriteTagHelperAttribute(), buffers the text internally, and HTML-attribute-encodes it before it is ever written to the response. It is not a real XSS sink.

Example

Given a controller action that binds user-provided input to a view model:

[HttpPost]
public IActionResult UpdateProfile(ProfileViewModel model)
{
    if (!ModelState.IsValid)
        return View(model);
    ...
}

and a Razor Pages/MVC view that renders one of its properties through a tag helper attribute:

<input asp-for="DisplayName" type="text" value="@Model.DisplayName" class="textEntry">

cs/web/xss previously reported:

User-provided value flows to here and is written to HTML or JavaScript: Microsoft.AspNetCore.Mvc.Razor.RazorPageBase.WriteLiteral() method.

This is a false positive. The value assigned to the asp-for attribute is captured into an internal string buffer via matching BeginWriteTagHelperAttribute() / EndWriteTagHelperAttribute() calls generated around the WriteLiteral call, and that buffered value is HTML-attribute-encoded before the tag helper's output is rendered, so it never reaches the response unencoded.

Fix

  • AspNetCore.qll: add getBeginWriteTagHelperAttributeMethod() / getEndWriteTagHelperAttributeMethod() to MicrosoftAspNetCoreMvcRazorPageBase.
  • Html.qll: add isBracketedForTagHelperAttribute() and use it to exclude bracketed WriteLiteral calls from MicrosoftAspNetRazorPageWriteLiteralSink. The predicate requires beginCall, writeLiteral, endCall to appear (in that order) in the same basic block, with no other Begin/EndWriteTagHelperAttribute call in between on either side, and all three calls to share an implicit this receiver, so an unrelated bracket (on this or another page instance) can't "adopt" an unbracketed call.

Test coverage

Added RazorTagHelperAttribute.cshtml / .cshtml.g.cs to the existing Security Features/CWE-079/XSS test, covering:

  • A bracketed WriteLiteral(model) (must not alert, suppressed FP).
  • An unbracketed WriteLiteral(model) (must alert, positive control).
  • Two independent bracket pairs back-to-back in one basic block (must not leak into each other).
  • A bare WriteLiteral(model) sandwiched between two unrelated brackets (must still alert).

Full Security Features/CWE-079 suite (6 tests) passes with no regressions. Also validated end-to-end against a small standalone ASP.NET Core Razor Pages reproduction project mirroring the example above: the false positive is no longer reported, while a genuine Html.Raw-based positive control in the same project remains reported.

Change note

Added csharp/ql/lib/change-notes/2026-09-18-razor-tag-helper-attribute-xss-fp.md (category: majorAnalysis).

felickz and others added 3 commits September 18, 2026 10:33
Razor's source generator brackets WriteLiteral calls that populate an
HTML attribute value on a tag-helper-enabled element (e.g. �sp-for)
between BeginWriteTagHelperAttribute()/EndWriteTagHelperAttribute()
calls. The captured text is buffered internally and HTML-attribute-
encoded before being rendered, so it is not a real XSS sink, but
cs/web/xss previously flagged it as one.

- Add getBeginWriteTagHelperAttributeMethod() /
  getEndWriteTagHelperAttributeMethod() to
  MicrosoftAspNetCoreMvcRazorPageBase.
- Add isBracketedForTagHelperAttribute() to Html.qll and use it to
  exclude bracketed WriteLiteral calls from
  MicrosoftAspNetRazorPageWriteLiteralSink, using same-basic-block,
  immediately-adjacent-bracket matching so unrelated bracket pairs
  cannot "adopt" an unbracketed call.
- Add RazorTagHelperAttribute.cshtml(.g.cs) test coverage: a
  suppressed bracketed write, an unbracketed positive control, two
  independent brackets in one basic block, and a bare write sandwiched
  between brackets.
- Add change note (majorAnalysis).

Diagnosed and validated against the real customer reproduction that
motivated this fix (field-security-codeql#257 / github#261): the false
positive (ChangeAccountInfo.cshtml) is no longer reported, while the
genuine Html.Raw-based positive control (ProfileSummary.cshtml)
remains reported. Full CWE-079 test suite (6 tests) passes with no
regressions.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot Code Review correctly flagged that isBracketedForTagHelperAttribute
never related the receivers of beginCall/writeLiteral/endCall, so a bracket
on one page instance could theoretically be mistaken for a bracket around a
WriteLiteral call on a different page (e.g. otherPage.BeginWriteTagHelperAttribute();
this.WriteLiteral(model); otherPage.EndWriteTagHelperAttribute();).

Require all three calls to have an implicit 	his qualifier, which is how
the Razor source generator always emits them, guaranteeing they act on the
same page instance. Re-verified: XSS.ql compiles, all 6 CWE-079 tests pass,
and the real customer reproduction database still shows the FP suppressed
and the genuine Html.Raw positive control still reported.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Shortened to a single terse sentence, matching the depth/style of other
recent change-notes (one bullet, no implementation detail), and called
out the ASP.NET Core Razor Pages/MVC scope.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@felickz
felickz marked this pull request as ready for review September 18, 2026 21:33
@felickz
felickz requested a review from a team as a code owner September 18, 2026 21:33
Copilot AI balanced review requested due to automatic review settings September 18, 2026 21:33

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The documentation incorrectly promises downstream HTML encoding instead of stating that the call only writes to a temporary buffer.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 1 Medium severity · 1 Low severity

Open (2)
What changed in this PR

Prevents false-positive C# XSS alerts for buffered Razor tag-helper attribute writes.

Changes:

  • Models Razor tag-helper attribute buffering methods.
  • Excludes correctly bracketed WriteLiteral calls from direct XSS sinks.
  • Adds regression fixtures and a change note.
File Description
Html.qll Detects buffered WriteLiteral calls.
AspNetCore.qll Models tag-helper buffering methods.
RazorTagHelperAttribute.cshtml Provides source-map fixture.
RazorTagHelperAttribute.cshtml.g.cs Adds generated-code test scenarios.
XSS.expected Updates expected query results.
Change note Documents the analysis change.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +187 to +190
* a `WriteLiteral` call does not write directly, unencoded, to the response: `WriteLiteral`
* appends to an internal string buffer, `EndWriteTagHelperAttribute()` returns that buffer, and
* the buffered text is subsequently stored as a tag helper attribute value and HTML-attribute-
* encoded when the tag helper's output is rendered. This is therefore not a real sink.
---
category: majorAnalysis
---
* Fixed a false positive in `cs/web/xss` for ASP.NET Core Razor Pages/MVC views: `WriteLiteral` calls generated for tag helper attribute values (for example, `asp-for`) are HTML-attribute-encoded before being rendered, so they are no longer treated as XSS sinks.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants