Sichere Bild-Uploads in ASP.NET Web Forms
Ein Upload-Ordner innerhalb einer ASP.NET-Website ist ein Ordner, aus dem IIS bereitwillig Code ausführt. Landet dort jemals eine .aspx-Datei, durch einen Fehler, per FTP oder durch eine spätere Änderung, kompiliert und führt IIS sie aus. Diese Seite zeigt die Prüfungen, die das Web-Forms-Beispiel vornimmt, bevor auch nur ein Byte geschrieben wird, und den web.config-Block, der uploads/ statisch hält, selbst wenn diese Prüfungen eines Tages umgangen werden. Jede Ebene haben wir unter IIS Express getestet.
Bevor etwas geschrieben wird
StorageRoot root = GetRoot(context); // 401 here first in per-user mode
if (file == null || file.ContentLength == 0)
throw new RteException(400, "No file was uploaded.");
// FileName is whatever the browser sent (old IE sends a full path). Don't feed it
// to Path.GetFileName: on .NET Framework that throws on characters like " < > |.
string clientName = file.FileName ?? "";
clientName = clientName.Substring(Math.Max(clientName.LastIndexOf('/'), clientName.LastIndexOf('\\')) + 1);
int dot = clientName.LastIndexOf('.');
string ext = dot >= 0 ? clientName.Substring(dot).ToLowerInvariant() : "";
if (Array.IndexOf(ImageExtensions, ext) < 0)
throw new RteException(400, "Only .jpg, .jpeg, .png, .gif and .webp images can be uploaded.");
if (file.ContentLength > MaxUploadBytes)
throw new RteException(413, "The image is larger than the 4 MB (4,000,000 bytes) limit.");
string dir = ResolveFolder(root, folder ?? "");
byte[] data = new byte[file.ContentLength];
Stream input = file.InputStream;
int read = 0;
while (read < data.Length)
{
int n = input.Read(data, read, data.Length - read);
if (n <= 0) break;
read += n;
}
// file.ContentType is chosen by the browser (or the attacker), so look at the bytes.
if (read != data.Length || !SignatureMatches(ext, data))
throw new RteException(400, "The file is not a valid " + ext.Substring(1).ToUpperInvariant() + " image.");- Allowlist für Dateiendungen: .jpg, .jpeg, .png, .gif, .webp. Eine
.svgwird abgelehnt, weil sie Skripte enthalten kann. Eine.phpoder.aspxwird abgelehnt, ganz gleich, welche Bytes sie enthält. - Größe: 4,000,000 Bytes, beantwortet mit einem JSON-413.
- Magic Bytes, nicht
ContentType.HttpPostedFile.ContentTypeist das, was der Browser gesendet hat. Wir haben PNG-Bytes mit der Angabetext/htmlgesendet, und genau das stand darin. Das Beispiel ignoriert den Wert und vergleicht die ersten Bytes mit der Signatur der Dateiendung:
static bool SignatureMatches(string ext, byte[] d)
{
switch (ext)
{
case ".jpg":
case ".jpeg":
return d.Length >= 3 && d[0] == 0xFF && d[1] == 0xD8 && d[2] == 0xFF;
case ".png":
return d.Length >= 8 && d[0] == 0x89 && d[1] == 0x50 && d[2] == 0x4E && d[3] == 0x47
&& d[4] == 0x0D && d[5] == 0x0A && d[6] == 0x1A && d[7] == 0x0A;
case ".gif":
return d.Length >= 6 && d[0] == 0x47 && d[1] == 0x49 && d[2] == 0x46 && d[3] == 0x38;
case ".webp":
return d.Length >= 12 && Encoding.ASCII.GetString(d, 0, 4) == "RIFF"
&& Encoding.ASCII.GetString(d, 8, 4) == "WEBP";
}
return false;
}Eine Datei namens holiday.jpg, die PHP-Quellcode enthält, scheitert an dieser Prüfung. Ein echtes JPEG mit angehängtem Inhalt besteht sie trotzdem. Die Signatur beweist den Typ, nicht, dass die Datei harmlos ist, und deshalb wird der Ordner selbst unten abgeriegelt.
Im selben Code stecken zwei Details von .NET Framework. Der Dateiname des Clients wird nie an Path.GetFileName übergeben, das unter .NET Framework 4.8 bei < oder | eine ArgumentException wirft. Der Upload-Handler liest außerdem Request.Unvalidated.Files, weil die Anforderungsüberprüfung auch Dateinamen erfasst: Eine Datei namens <script>x.png ergab sonst eine HTML-Seite mit 500. Der Name wird auf a-z0-9- reduziert und um 8 zufällige Hex-Zeichen ergänzt, sodass nichts, was der Client gewählt hat, auf die Festplatte gelangt.
Wohin geschrieben werden darf
Das Feld folder durchläuft dasselbe ResolveFolder wie die Galerie. Jedes Segment muss ein gültiger Ordnername sein, und das Ergebnis von Path.GetFullPath muss innerhalb des Stammordners bleiben. Galerieordner in Web Forms zeigt den Code. Im Vertragstest erhielt ein Upload mit folder=../ ein 400 und ein Ordner, der nicht existiert, ein 404.
uploads/ liefert Dateien aus und führt nichts aus
IIS entscheidet anhand der Handler-Zuordnung, was ausgeführt wird, nicht anhand des Ordners. Zur Prüfung haben wir eine einzeilige Seite, <%= 6*7 %>, als probe-root.aspx im Stammverzeichnis der Website abgelegt. Der Request gab 42 zurück: Sie wurde ausgeführt. Die web.config des Beispiels verhindert das für uploads/:
<!-- Uploaded files are served as static files and nothing else. Clearing the handler list
means that even if a .aspx, .ashx or .config ever lands in uploads/, IIS will not run
it - it gets 404, not executed. The extension allow-list also refuses to serve
anything that is not an image (.html, .svg, .txt ...), wherever it came from. -->
<location path="uploads">
<system.webServer>
<handlers>
<clear />
<add name="StaticFile" path="*" verb="GET,HEAD" modules="StaticFileModule" resourceType="File" requireAccess="Read" />
</handlers>
<security>
<requestFiltering>
<fileExtensions allowUnlisted="false">
<add fileExtension=".jpg" allowed="true" />
<add fileExtension=".jpeg" allowed="true" />
<add fileExtension=".png" allowed="true" />
<add fileExtension=".gif" allowed="true" />
<add fileExtension=".webp" allowed="true" />
</fileExtensions>
</requestFiltering>
</security>
<httpProtocol>
<customHeaders>
<add name="X-Content-Type-Options" value="nosniff" />
</customHeaders>
</httpProtocol>
</system.webServer>
</location><clear />plus nurStaticFile. Bevor die Liste der Dateiendungen hinzukam, antworteten dieselbe.aspxund eine.ashxinuploads/mit 404.3. Es gab keinen Handler, der sie ausführte, und keinen MIME-Typ, um sie als Text zu senden.fileExtensions allowUnlisted="false". Mit der Allowlist antwortetenuploads/probe.aspxund ein alsuploads/probe.txtgespeicherter HTML-Payload beide mit 404.7, während.pngund.webpausgeliefert wurden.X-Content-Type-Options: nosniffwar bei den ausgelieferten Bildern vorhanden, sodass ein Browser keinen anderen Typ aus dem Inhalt errät.
Der Abschnitt handlers ist in der applicationhost.config von IIS standardmäßig gesperrt, aber deren Block <location path="" overrideMode="Allow"> entsperrt ihn für Websites, weshalb eine web.config ihn ändern darf. Das haben wir in der Vorlage von IIS Express geprüft, einen vollständigen IIS-Server haben wir nicht getestet. Auf einem Host, der den Abschnitt sperrt, schlägt die Website mit einem Konfigurationsfehler 500.19 fehl, statt stillschweigend Skripte auszuführen.
Testen
Während das Beispielprojekt zu dieser Anleitung auf Port 5401 läuft:
echo "<script>alert(1)</script>" > fake.png
curl -F "file=@fake.png" http://localhost:5401/rte/upload
{"error":"The file is not a valid PNG image."}
curl -F "file=@fake.png;filename=x.svg" http://localhost:5401/rte/upload
{"error":"Only .jpg, .jpeg, .png, .gif and .webp images can be uploaded."}Beide Antworten sind HTTP 400. Um die Abriegelung des Ordners zu sehen, kopieren Sie eine beliebige .aspx nach uploads\ und fordern Sie sie an. Sie erhalten die Seite 404.7 von IIS, nicht die Ausgabe der Seite. Im Galerie-Dialog zeigt eine abgelehnte Datei „Upload failed for <name>.“ an. Der Dialog verwendet seine eigene Meldung statt des Textes des Servers.
Dieselbe Anleitung für andere Plattformen
JavaScript · Next.js · PHP · ASP.NET Core · Razor Pages · MVC 5