Carga segura de imágenes en Razor Pages: antiforgery y origen
Razor Pages protege cada handler POST con un token antiforgery, y la mayoría de los desarrolladores de Razor Pages nunca piensan en ello porque el tag helper <form> añade el token por ellos. Las cargas de imágenes y las solicitudes de New folder del editor no son formularios: son XHR que construye el propio editor, y la versión actual no puede añadirles un token. Esta página muestra cómo el ejemplo mantiene protegida la página, qué hace en su lugar con esas solicitudes y qué haría falta para usar el token de todos modos.
Qué comprueba Razor Pages
En un POST a un page handler, Razor Pages compara dos valores: una cookie y un token de solicitud, que de forma predeterminada llega como el campo de formulario __RequestVerificationToken o como el encabezado RequestVerificationToken. Mostramos los valores predeterminados de AntiforgeryOptions en .NET 8: esos dos nombres, y SameSite=Strict en la cookie. En el ejemplo, el formulario Save de la página pasa la comprobación, y un simple curl -d "Html=x" a la página recibe un 400.
// Razor Pages validates the antiforgery token on this POST by default; the <form> tag helper
// in Index.cshtml adds the hidden __RequestVerificationToken field. Save Html to your database here.
public void OnPost() => Message = $"Saved {Html?.Length ?? 0} characters of HTML.";Por qué las solicitudes del editor no llevan token
La solicitud de carga viene de rte-files.js, y la de New folder, del plugin de la galería. Ambos crean su propio FormData y su propio XMLHttpRequest, y ninguno, en la versión actual, tiene una opción para encabezados o campos adicionales. Modificar XMLHttpRequest de forma global para inyectar el token cambiaría todas las solicitudes de la página, así que el ejemplo no lo hace.
Eso deja las tres rutas como endpoints fuera de la página, con la validación del token desactivada de forma explícita. En .NET 8, eso es obligatorio para los endpoints que enlazan IFormFile o [FromForm]: sin .DisableAntiforgery(), una aplicación Razor Pages respondía 500 cuando faltaba app.UseAntiforgery(), y 400 cuando estaba presente.
// Binding [FromForm] or IFormFile makes .NET 8 demand an antiforgery token on these endpoints.
// The editor's XHRs cannot send one, so it is switched off here and RteFiles checks the
// request's origin instead (see RequireSameOrigin). guide-notes.md has the details.
rte.MapPost("/gallery", (HttpContext http, [FromForm] string? action, [FromForm] string? folder, [FromForm] string? name) =>
Json(RteFiles.CreateFolder(http, action, folder, name)))
.DisableAntiforgery();La comprobación que sustituye al token
// CSRF: the editor's upload and create-folder requests are XHRs that carry no antiforgery token,
// so these POSTs refuse requests that a browser marks as coming from another site instead.
// Browsers send Sec-Fetch-Site and Origin on these requests; tools like curl send neither.
static void RequireSameOrigin(HttpRequest request)
{
var site = request.Headers["Sec-Fetch-Site"].ToString();
var origin = request.Headers.Origin.ToString();
var foreign = site is "cross-site" or "same-site"
|| (origin.Length > 0 && !(Uri.TryCreate(origin, UriKind.Absolute, out var o)
&& string.Equals(o.Authority, request.Host.Value, StringComparison.OrdinalIgnoreCase)));
if (foreign) throw new RteError(403, "Cross-site request refused.");
}Una falsificación de solicitudes entre sitios es una página de otro sitio que hace que el navegador del usuario envíe solicitudes al tuyo. Los navegadores etiquetan esas solicitudes: Sec-Fetch-Site dice si la solicitud viene del mismo origen, del mismo sitio o de otro sitio, y Origin indica el origen de la página. Un script del otro sitio no puede establecer ninguno de los dos encabezados. Así que las rutas de carga y de New folder responden 403 cuando Sec-Fetch-Site es cross-site o same-site, o cuando Origin es un host distinto. Las cargas desde la propia página del ejemplo pasaron en Chromium. Las solicitudes de curl o de código de servidor no envían ninguno de los dos encabezados y se permiten, ya que no pueden llevar las cookies del usuario.
Dos cosas que conviene saber antes de confiar en ello:
- Detrás de un proxy inverso,
Request.Hostdebe ser el host público. AñadeUseForwardedHeaders, o la comprobación rechazará tus propias páginas. same-sitese rechaza a propósito: un subdominio hermano comoblog.example.comes otra aplicación, aunque comparta cookies.
Usar el token en su lugar
Si rte-files.js y el plugin de la galería admitieran encabezados de solicitud adicionales, la página podría pasar el token de solicitud de IAntiforgery.GetAndStoreTokens(HttpContext).RequestToken en el encabezado RequestVerificationToken. Entonces quitarías .DisableAntiforgery() y añadirías app.UseAntiforgery(), y los endpoints validarían el token como hace la página. La versión actual no tiene esa opción, así que el ejemplo no puede mostrarlo funcionando.
Las demás comprobaciones
El origen es la primera de varias comprobaciones. Después, RteFiles.Save comprueba la carpeta, el tamaño, la extensión y los primeros bytes, guarda el archivo con un nombre generado, y la carpeta se sirve con solo cinco tipos de contenido y nosniff. Carga segura de imágenes en ASP.NET Core las repasa; el código es el mismo en este ejemplo.
Pruébalo
Ejecuta el proyecto de ejemplo de esta guía con dotnet run y luego haz como si fueras otro sitio y envía a la página sin token:
curl -H "Origin: https://evil.example" -F "action=create-folder" -F "folder=" -F "name=x" http://localhost:5303/rte/gallery
{"error":"Cross-site request refused."}
curl -s -o /dev/null -w "%{http_code}\n" -d "Html=x" http://localhost:5303/
400La primera es el 403 del ejemplo. La segunda es la propia comprobación antiforgery de Razor Pages en la página.
La misma guía para otras plataformas
JavaScript · Next.js · PHP · ASP.NET Core · MVC 5 · Web Forms