Skip to content

System.Web.HttpRequest.get_RawUrl (C#) modeled as url-redirection barrier causes false-negative open-redirect alerts #22174

Description

@wildoranges

Description of the issue

System.Web.HttpRequest.get_RawUrl (the C# property Request.RawUrl) is modeled as a barrierModel for url-redirection in csharp/ql/lib/ext/System.Web.model.yml (i.e. treated as a sanitizer). But RawUrl is not a URL-redirect sanitizer — it is the attacker-controlled, un-normalized HTTP request-line path (including the query string), which can be a protocol-relative URL such as //evil.com that browsers resolve to {current-scheme}://evil.com and navigate to an external site. So Response.Redirect(Request.RawUrl) is an open-redirect vulnerability, but the barrierModel suppresses the alert, causing CodeQL to miss it.

CodeQL's own test suite marks ctx.Response.Redirect(ctx.Request.RawUrl) as GOOD (non-vulnerable), with the comment "Redirecting to the RawUrl only reloads the current Url" — but that assumption is wrong, because RawUrl is the attacker-controlled request line, not a fixed current-page URL. I confirmed the false negative by removing the barrierModel row and re-running the test — the suppressed alert appears.

The model

csharp/ql/lib/ext/System.Web.model.yml (the barrierModel block, with CodeQL's own comment):

extensions:
  - addsTo:
      pack: codeql/csharp-all
      extensible: barrierModel
    data:
      # The RawUrl property is considered to be safe for URL redirects
      - ["System.Web", "HttpRequest", False, "get_RawUrl", "()", "", "ReturnValue", "url-redirection", "manual"]

The barrierModel means CodeQL considers the return value of RawUrl to be safe from url-redirection (taint of kind url-redirection is stopped at the barrier).

Why Request.RawUrl is not a URL-redirect sanitizer

RawUrl is not a sanitizer — it is the attacker-controlled, un-normalized request-line path. The evidence chain is as follows:

(1) RawUrl is the attacker-controlled request-line path, un-normalized. Per the Microsoft docs, "The raw URL is defined as the part of the URL following the domain information" — the part of the request line after the domain (path + query string). The attacker fully controls the HTTP request line; sending GET //evil.com?x=1 HTTP/1.1 makes Request.RawUrl = //evil.com?x=1. The SimpleWorkerRequest.GetRawUrl() docs explicitly warn: "The returned URL is not normalized. Using the URL for access control or security-sensitive decisions can expose your application to canonicalization security vulnerabilities." In the ASP.NET reference source (HttpRequest.cs), RawUrl is taken directly from the worker request: _rawUrl = _wr.GetRawUrl(), with the comment "RAW Url (as supplied by worker request)" — supplied by the client, un-sanitized.

(2) //evil.com is a protocol-relative URL that browsers navigate to an external site. Per RFC 3986 §4.2, a URL beginning with // is a scheme-relative URL; the browser resolves it using the current page's scheme, so //evil.com is resolved as {current-scheme}://evil.com and navigates to evil.com. The OWASP Open Redirect Cheat Sheet lists "Protocol-relative URL //evil.com" as a standard open-redirect bypass technique.

(3) System.Web.HttpResponse.Redirect writes //evil.com to the Location header unchanged. This is the sink used by the test code ctx.Response.Redirect (System.Web, ASP.NET Framework, not Core). I traced the ASP.NET Framework reference source HttpResponse.cs Redirect(string url, bool endResponse, bool permanent) method; it processes the url in four steps, and //evil.com passes through each unchanged:

  • ApplyAppPathModifier("//evil.com"): the source explicitly handles URLs beginning with //if (!UrlPath.IsRooted(virtualPath) || virtualPath.StartsWith("//", ...)) { return virtualPath; }, with the comment "ignore paths with http://server/... or //". //evil.com begins with //, so it is returned unchanged.
  • ConvertToFullyQualifiedRedirectUrlIfRequired("//evil.com"): under the default configuration (UseFullyQualifiedRedirectUrl=false, the vast majority of apps) it does return url, unchanged. (Even with that config enabled, new Uri(Request.Url, "//evil.com").AbsoluteUri resolves //evil.com as a protocol-relative URL per RFC 3986, yielding http://evil.com, which still navigates externally.)
  • UrlEncodeRedirect("//evil.com"): TrySplitUriForPathEncode splits //evil.com into schemeAndAuthority="//evil.com" + path="", and UrlEncodeNonAscii only encodes non-ASCII characters; //evil.com is all-ASCII, so it is unchanged.
  • RedirectLocation = urlheaders.Add(new HttpResponseHeader(HttpWorkerRequest.HeaderLocation, _redirectLocation)): //evil.com is written to the Location header unchanged.

Result: HTTP 302 Location: //evil.com, which the browser resolves per RFC 3986 §4.2 as {current-scheme}://evil.com and navigates to evil.com.

Supplementary: dotnet/aspnetcore#66486 (the same class of issue in ASP.NET Core) confirms that //host-style URLs bypass the scheme-delimiter check and flow into the Location header; that issue also notes that ASP.NET's own SharedUrlHelper.IsLocalUrl already rejects URLs beginning with // or /\ — indicating ASP.NET itself considers //-prefixed URLs non-local and dangerous (IsLocalUrl treats //evil.com as non-local, but Response.Redirect performs no such check).

(4) CodeQL's own threat model also treats "attacker can control the location" as dangerous. In csharp/ql/lib/semmle/code/csharp/security/dataflow/UrlRedirectQuery.qll, the ConcatenationSanitizer (which marks "foo.asp?param=" + url as GOOD) has the comment: "the attacker can then only control the query string parameters, rather than the location itself. In the majority of cases, this will only allow the attacker to redirect the user to a link they could have already redirected them to." So CodeQL's model is: controlling the location = dangerous, controlling only the query = safe. RawUrl includes the path (i.e. the location), not just the query, so by CodeQL's own model RawUrl is dangerous.

In summary, Response.Redirect(Request.RawUrl) is a real open-redirect vulnerability when RawUrl = "//evil.com", and the barrierModel suppresses the alert.

Confirmed false negative (with ablation)

csharp/ql/test/query-tests/Security Features/CWE-601/UrlRedirect/UrlRedirect.cs lines 41-42:

// GOOD: Redirecting to the RawUrl only reloads the current Url
ctx.Response.Redirect(ctx.Request.RawUrl);
  • ctx.Request.RawUrl is attacker-controlled (the request-line path, un-normalized)
  • ctx.Response.Redirect is a url-redirection sink
  • UrlRedirect.expected reports url-redirection alerts for UrlRedirect.cs lines 13, 23, 38, 39, 48, 64, 70, 76, but not line 42 (Redirect(Request.RawUrl))

The comment "Redirecting to the RawUrl only reloads the current Url" is incorrect — RawUrl is not a fixed current-page URL; it is the attacker-controlled request line and can be //evil.com.

Ablation confirmation: I removed the get_RawUrl barrierModel block from csharp/ql/lib/ext/System.Web.model.yml and re-ran the test. The .actual output now includes a line-42 alert:

| UrlRedirect.cs:42:31:42:48 | access to property RawUrl | UrlRedirect.cs:42:31:42:48 | access to property RawUrl | UrlRedirect.cs:42:31:42:48 | access to property RawUrl | Untrusted URL redirection due to $@. | UrlRedirect.cs:42:31:42:48 | access to property RawUrl | user-provided value |

This confirms the barrierModel is suppressing a legitimate url-redirection alert. The taint flows Request.RawUrl (attacker-controlled request line) → Response.Redirect, and RawUrl can be //evil.com (a protocol-relative URL that navigates externally).

Re-adding the barrierModel block restores the baseline (line 42 is no longer flagged, test passes) — the alert appears and disappears exactly with the barrier, confirming it is the cause of the false negative.

Steps to reproduce

  1. Run the UrlRedirect query on the existing test:

    codeql test run "csharp/ql/test/query-tests/Security Features/CWE-601/UrlRedirect/UrlRedirect.qlref"
    

    Observe that line 42 (ctx.Response.Redirect(ctx.Request.RawUrl)) is not in the alerts (UrlRedirect.cs flags only lines 13, 23, 38, 39, 48, 64, 70, 76). The test passes (.actual matches .expected).

  2. Remove the entire barrierModel block (the get_RawUrl row) from csharp/ql/lib/ext/System.Web.model.yml and re-run. The test now fails with an unexpected alert on line 42 (Unexpected result: Alert on access to property RawUrl, "Untrusted URL redirection").

  3. Re-add the barrierModel block and re-run; the alert disappears and the test passes again (baseline restored).

Context

  • Tested with CodeQL CLI 2.25.6, repo HEAD a24d222d96 (codeql-cli/latest-577-ga24d222d96).
  • The barrierModel for get_RawUrl was added in commit 130f8f148b ("Convert barrier to MaD").
  • Every link in the attack chain has independent evidence: MS docs (RawUrl is un-normalized; not for security-sensitive decisions), RFC 3986 §4.2 (// is a protocol-relative URL), OWASP (protocol-relative URL is a standard open-redirect bypass), ASP.NET Framework HttpResponse.Redirect source verified function-by-function (ApplyAppPathModifier comment "ignore //", StartsWith("//") returns unchanged; all four steps pass //evil.com through unchanged into the Location header), URL Rewrite middleware does not detect scheme-relative URLs (//host) in redirect targets dotnet/aspnetcore#66486 (the Core equivalent + IsLocalUrl rejecting //), and CodeQL's own UrlRedirectQuery.qll threat model (controlling the location is dangerous).

Notes

  • The test comment "Redirecting to the RawUrl only reloads the current Url" reflects a common misconception: that RawUrl is always the current page's path. But RawUrl is the request line's path+query, and the attacker can send any request line (e.g. GET //evil.com HTTP/1.1), making RawUrl = //evil.com.
  • The safe approach is to validate with UrlHelper.IsLocalUrl() before redirecting (CodeQL's UrlRedirectQuery.qll already models IsLocalUrl as a sanitizer), rather than assuming RawUrl itself is safe.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions