Sicherer Bild-Upload in Razor Pages: Antiforgery und Origin

Diagramm: Der Formular-POST der Seite enthält das Antiforgery-Token und wird von Razor Pages validiert, während die XHRs des Editors an Endpoints gehen, die Sec-Fetch-Site und Origin prüfen

Razor Pages schützt jeden POST-Handler mit einem Antiforgery-Token, und die meisten Razor-Pages-Entwickler denken nie darüber nach, weil der Tag-Helper <form> das Token für sie hinzufügt. Die Bild-Uploads und die Requests für „New folder“ des Editors sind keine Formulare: Es sind XHRs, die der Editor selbst erzeugt, und das aktuelle Release kann ihnen kein Token hinzufügen. Diese Seite zeigt, wie das Beispiel die Seite geschützt hält, was es stattdessen für diese Requests tut und was nötig wäre, um das Token doch zu verwenden.

Was Razor Pages prüft

Bei einem POST an einen Page-Handler vergleicht Razor Pages zwei Werte: ein Cookie und ein Request-Token, das standardmäßig als Formularfeld __RequestVerificationToken oder als Header RequestVerificationToken ankommt. Wir haben die Standardwerte von AntiforgeryOptions in .NET 8 ausgegeben: diese beiden Namen und SameSite=Strict für das Cookie. Im Beispiel besteht das Save-Formular der Seite die Prüfung, und ein bloßes curl -d "Html=x" an die Seite erhält ein 400.

C# · Pages/Index.cshtml.cs
// 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.";

Warum die Requests des Editors kein Token haben

Der Upload-Request kommt von rte-files.js, der Request für „New folder“ vom Galerie-Plugin. Beide erstellen ihr eigenes FormData und ihren eigenen XMLHttpRequest, und keines von beiden hat im aktuellen Release eine Option für zusätzliche Header oder Felder. XMLHttpRequest global zu patchen, um das Token einzuschleusen, würde jeden Request auf der Seite verändern, deshalb tut das Beispiel das nicht.

Damit bleiben die drei Routen Endpoints außerhalb der Seite, mit ausdrücklich abgeschalteter Token-Validierung. In .NET 8 ist das für Endpoints erforderlich, die IFormFile oder [FromForm] binden: Ohne .DisableAntiforgery() antwortete eine Razor-Pages-App mit 500, wenn app.UseAntiforgery() fehlte, und mit 400, wenn es vorhanden war.

C# · RteEndpoints.cs
// 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();

Die Prüfung, die das Token ersetzt

C# · RteFiles.cs
// 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.");
}

Bei Cross-Site Request Forgery bringt eine Seite auf einer anderen Website den Browser des Benutzers dazu, an Ihre Website zu senden. Browser kennzeichnen solche Requests: Sec-Fetch-Site gibt an, ob der Request vom selben Origin, von derselben Site oder von einer anderen Site kommt, und Origin nennt den Origin der Seite. Ein Skript auf der anderen Website kann keinen der beiden Header setzen. Deshalb antworten die Routen für Upload und „New folder“ mit 403, wenn Sec-Fetch-Site cross-site oder same-site ist oder Origin ein anderer Host ist. Uploads von der eigenen Seite des Beispiels kamen in Chromium durch. Requests von curl oder aus Servercode senden keinen der beiden Header und werden zugelassen, da sie die Cookies des Benutzers nicht mitführen können.

Zwei Dinge sollten Sie wissen, bevor Sie sich darauf verlassen:

  • Hinter einem Reverse Proxy muss Request.Host der öffentliche Host sein. Ergänzen Sie UseForwardedHeaders, sonst lehnt die Prüfung Ihre eigenen Seiten ab.
  • same-site wird absichtlich abgelehnt: Eine benachbarte Subdomain wie blog.example.com ist eine andere Anwendung, auch wenn sie Cookies teilt.

Stattdessen das Token verwenden

Könnten rte-files.js und das Galerie-Plugin zusätzliche Request-Header aufnehmen, könnte die Seite das Request-Token aus IAntiforgery.GetAndStoreTokens(HttpContext).RequestToken im Header RequestVerificationToken übergeben. Dann würden Sie .DisableAntiforgery() entfernen und app.UseAntiforgery() hinzufügen, und die Endpoints würden das Token validieren wie die Seite. Das aktuelle Release hat keine solche Option, daher kann das Beispiel das nicht funktionierend zeigen.

Die anderen Prüfungen

Der Origin ist die erste von mehreren Prüfungen. Danach prüft RteFiles.Save Ordner, Größe, Dateiendung und die ersten Bytes und speichert die Datei unter einem generierten Namen, und der Ordner wird mit nur fünf Content Types und nosniff ausgeliefert. Sicherer Bild-Upload in ASP.NET Core geht sie durch; der Code ist in diesem Beispiel derselbe.

Testen

Starten Sie das Beispielprojekt zu dieser Anleitung mit dotnet run, geben Sie sich dann als andere Website aus und senden Sie ohne Token an die Seite:

Shell
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/
400

Das erste ist das 403 des Beispiels. Das zweite ist die eigene Antiforgery-Prüfung von Razor Pages für die Seite.

Dieselbe Anleitung für andere Plattformen

JavaScript · Next.js · PHP · ASP.NET Core · MVC 5 · Web Forms

Weiterlesen