Cross Site Scripting (XSS) en ASP.NET

En una entrada anterior había prometido comentar un pequeño bug presente en asp.net 2 -y probablemente versiones anteriores.

El escenario de este problema es el siguiente, un hábil desarrollador hace una pequeña página que se encarga de redireccionar a una URL que pasa como parámetro, ejemplo:

csharp:
<%@ Page Language="C#" %>
<script runat="server">
    void Page_Load()
    {
        Response.Redirect(Request.Params["uri"]);
    }
</script>
Bien, si nos ponemos a analizar el código generado con Reflector, podremos sacar unas cuantas conclusiones:
csharp:
// Comentarios añadidos
public void Redirect(string url, bool endResponse)
{
      if (url == null)
      {
            throw new ArgumentNullException("url");
      }
      if (url.IndexOf('\n') >= 0) // no se permiten saltos de línea
      {
            throw new ArgumentException(HttpRuntime.FormatResourceString("Cannot_redirect_to_newline"));
      }
      if (this._headersWritten)
      {
            throw new HttpException(HttpRuntime.FormatResourceString("Cannot_redirect_after_headers_sent"));
      }
      url = this.ApplyAppPathModifier(url); // determina la ruta correcta si es una URL relativa
      url = this.ConvertToFullyQualifiedRedirectUrlIfRequired(url); // Usa la url absoluta si está definida en el web.config
      url = this.UrlEncodeRedirect(url); // Codifica espacios y caracteres especiales (ascii > 128)
      this.Clear();
      Page page1 = this._context.Handler as Page;
      if (((page1 != null) && page1.IsPostBack) && page1.SmartNavigation)
      {
            this.Write("<BODY><ASP_SMARTNAV_RDIR url=\"");
            this.Write(url);
            this.Write("\"></ASP_SMARTNAV_RDIR>");
            this.Write("</BODY>");
      }
      else
      {
            this.StatusCode = 0x12e; // Código HTTP 302
            this._redirectLocation = url; // Se envía la cabecera Location con este valor
            this.Write("<html><head><title>Object moved</title></head><body>\r\n");
            // HttpUtility.HtmlEncode codifica sólo algunos caracteres (<, >, &, etc)
X            this.Write("<h2>Object moved to <a href='" + HttpUtility.HtmlEncode(url) + "'>here</a>.</h2>\r\n");
            this.Write("</body></html>\r\n");
      }
      if (endResponse)
      {
            this.End();
      }
}

En la línea X del código mostrado, se puede observar que el desarrollador, en mi opinión, cometió un error al usar HttpUtility.HtmlEncode en lugar de HttpUtility.UrlEncode, puesto que si usamos javascript:alert("XSS") como valor para el parámetro uri del primer bloque de código, veremos que en Firefox se mostrará un mensaje XSS al hacer click en el enlace "here".

He puesto una prueba de concepto (abrir con Firefox, no funciona en IE) en un servidor gratuito con soporte para ASP.NET 2.