Envoi d’images sécurisé avec Razor Pages : antifalsification et origine
Razor Pages protège chaque gestionnaire POST par un jeton antifalsification, et la plupart des développeurs Razor Pages n’y pensent jamais, car le tag helper <form> ajoute le jeton pour eux. Les envois d’images et les requêtes New folder de l’éditeur ne sont pas des formulaires : ce sont des XHR que l’éditeur construit lui-même, et la version actuelle ne peut pas y ajouter de jeton. Cette page montre comment l’exemple garde la page protégée, ce qu’il fait à la place pour ces requêtes, et ce qu’il faudrait pour utiliser malgré tout le jeton.
Ce que vérifie Razor Pages
Lors d’un POST vers un gestionnaire de page, Razor Pages compare deux valeurs : un cookie et un jeton de requête, qui arrive par défaut dans le champ de formulaire __RequestVerificationToken ou dans l’en-tête RequestVerificationToken. Nous avons affiché les valeurs par défaut d'AntiforgeryOptions en .NET 8 : ces deux noms, et SameSite=Strict sur le cookie. Dans l’exemple, le formulaire Save de la page passe la vérification, et un simple curl -d "Html=x" vers la page reçoit 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.";Pourquoi les requêtes de l’éditeur n’ont pas de jeton
La requête d’envoi provient de rte-files.js et la requête New folder du plugin de galerie. Les deux créent leur propre FormData et leur propre XMLHttpRequest, et aucun des deux, dans la version actuelle, n’a d’option pour des en-têtes ou des champs supplémentaires. Modifier XMLHttpRequest globalement pour injecter le jeton changerait toutes les requêtes de la page : l’exemple ne le fait donc pas.
Il reste donc les trois routes, sous forme de points de terminaison extérieurs à la page, avec la validation du jeton explicitement désactivée. En .NET 8, c’est obligatoire pour les points de terminaison qui lient IFormFile ou [FromForm] : sans .DisableAntiforgery(), une application Razor Pages a répondu 500 en l’absence d'app.UseAntiforgery(), et 400 en sa présence.
// 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 vérification qui remplace le jeton
// 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.");
}Une falsification de requête intersite, c’est une page d’un autre site qui fait envoyer au navigateur de l’utilisateur une requête vers le vôtre. Les navigateurs étiquettent ces requêtes : Sec-Fetch-Site indique si la requête provient de la même origine, du même site ou d’un autre site, et Origin donne l’origine de la page. Le script de l’autre site ne peut définir ni l’un ni l’autre de ces en-têtes. Les routes d’envoi et de New folder répondent donc 403 lorsque Sec-Fetch-Site vaut cross-site ou same-site, ou lorsque Origin désigne un autre hôte. Les envois depuis la page de l’exemple elle-même sont passés dans Chromium. Les requêtes de curl ou du code serveur n’envoient aucun de ces en-têtes et sont autorisées, car elles ne peuvent pas porter les cookies de l’utilisateur.
Deux choses à savoir avant de vous y fier :
- Derrière un proxy inverse,
Request.Hostdoit être l’hôte public. AjoutezUseForwardedHeaders, sinon la vérification refuse vos propres pages. same-siteest refusé volontairement : un sous-domaine voisin commeblog.example.comest une autre application, même s’il partage des cookies.
Utiliser plutôt le jeton
Si rte-files.js et le plugin de galerie pouvaient accepter des en-têtes de requête supplémentaires, la page pourrait transmettre le jeton de requête issu de IAntiforgery.GetAndStoreTokens(HttpContext).RequestToken dans l’en-tête RequestVerificationToken. Vous supprimeriez alors .DisableAntiforgery() et ajouteriez app.UseAntiforgery(), et les points de terminaison valideraient le jeton comme le fait la page. La version actuelle n’a pas d’option de ce type : l’exemple ne peut donc pas montrer ce fonctionnement.
Les autres vérifications
L’origine est la première de plusieurs vérifications. Ensuite, RteFiles.Save vérifie le dossier, la taille, l’extension et les premiers octets, et stocke le fichier sous un nom généré ; le dossier est servi avec seulement cinq types de contenu et nosniff. Envoi d’images sécurisé avec ASP.NET Core les passe en revue ; le code est le même dans cet exemple.
Tester
Lancez le projet d’exemple de ce guide avec dotnet run, puis faites-vous passer pour un autre site et envoyez une requête POST à la page sans jeton :
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 première réponse est le 403 de l’exemple. La seconde est la vérification antifalsification propre à Razor Pages sur la page.
Le même guide pour d’autres plateformes
JavaScript · Next.js · PHP · ASP.NET Core · MVC 5 · Web Forms