Programmiertechnik/Kontextwechsel/erkennen und behandeln

Aus SELFHTML-Wiki
Wechseln zu: Navigation, Suche

Kontextwechsel erkennen und behandeln

Jeder unberücksichtigte Kontextwechsel kann einerseits zu einer Fehlinterpretation beim Datenempfänger führen, andererseits potenziell eine Lücke sein, die das unbeabsichtigte Einfügen von Code ermöglicht. Deshalb ist es wichtig, die Kontextwechsel zu erkennen, um sie angemessen behandeln zu können. Es ist empfehlenswert, die Behandlung stets vorzunehmen, auch wenn sie im Moment für einen bestimmten Fall überflüssig erscheint, weil die Daten keine kritischen Zeichen enthalten. Damit spart man sich das böse Erwachen, wenn sich später die Daten ändern und man nicht alle Stellen nachgebessert hat.

Die Aufbereitung der Daten für einen anderen Kontext sollte erst zum Zeitpunkt der Zusammenstellung der Ausgabedaten erfolgen. Denn die aufbereiteten Daten sind in aller Regel nur für das jeweilige Ausgabemedium nützlich. Im Rest des Programms wird man mit den Rohdaten arbeiten wollen, denn sonst stören die zusätzlichen Maskierungen die normale Verarbeitung. Beispielsweise liefert eine Zeichenzählung ein anderes Ergebnis.

Im Webumfeld auftretende Kontextwechsel sind unter anderem:

SQL

MySQL wird in den Abschnitten Fehler und deren Auswirkungen sowie Verhindernde Maßnahmen beschrieben.

Andere Datenbanksysteme haben teilweise ihre eigenen Regeln. Mitunter stellt die DBMS-spezifische PHP-Extension dafür vorgesehene Funktionen bereit. Eine unvollständige Aufzählung:

SQLite stellt die Funktion sqlite_escape_string() zur Verfügung. SQLite3 ist eine objektorientierte Implementation. Es kennt die Methode SQLite3::escapeString(). Außerdem ist die Verwendung von Prepared Statements möglich.

Unter PostgreSQL können die Funktion pg_escape_string() sowie Prepared Statements verwendet werden.

Oracle OCI8 hat einerseits keine Maskierfunktion und bietet andererseits die Ausführung von SQL-Statements nur als Prepared Statements an. Sollen Werte „händisch“ und unter Umgehung der Platzhalterfunktionalität in ein SQL-Statement eingefügt werden, so sind diese ebenfalls „händisch“ gemäß den Oracle-Regeln zu behandeln.

Mssql für Microsofts SQL Server bringt zwar mit mssql_query() die Möglichkeit zum direkten Ausführen von SQL-Statements mit, aber keine Funktion zum Maskieren von Daten. Stringwerte werden in einfache Anführungszeichen gesetzt, ein einfaches Anführungszeichen innerhalb den Daten ist zu verdoppeln. Allerdings können auch Prepared Statements ausgeführt werden.

Die Datenbank-Abstraktion ODBC kann unterschiedliche DBMS ansprechen und offeriert keine Maskierfunktion. Prepared Statements können verwendet werden.

Auch PDO ist eine Datenbank-Abstraktion – eine objektorientierte. Eine Besonderheit ist die Methode PDO::quote(). Sie sorgt sowohl für die Maskierung der Daten als auch für die DBMS-gerechte Quotierung (Einfassen in Stringbegrenzungszeichen). Wichtig ist jedoch, dass die Zeichenkodierung der Datenbankverbindung gesetzt wurde, falls der jeweilige PDO-Treiber das erfordert.
In der PDO-Dokumentation wird jedoch ausdrücklich empfohlen, stattdessen Prepared Statements zu verwenden.

HTML

Für HTML gibt es die Funktionen htmlspecialchars() und htmlentities(). Die HTML-eigenen Zeichen werden von htmlspecialchars() berücksichtigt. Zusätzlich dazu wandelt htmlentities() noch eine Menge anderer Zeichen in eine HTML-Darstellung, doch das ist meist nicht notwendig. (Das passende Thema dazu wäre Zeichencodierung, das jedoch nicht im Fokus dieses Artikels liegt.)

Es ist übrigens nicht in jedem Fall erforderlich, alle HTML-eigenen Zeichen als Entity oder NCR (Numeric Character Reference: &#…;) zu notieren. Die Anführungszeichen müssen beispielsweise zwingend nur in einem mit gleichem Anführungszeichen eingefassten Attributwert umgeschrieben werden. In Fließtext ist das nicht erforderlich. Es schadet aber auch nicht, htmlspecialchars() sowohl für Attributwerte als auch für Fließtext anzuwenden.

Beispiel
printf('<input type="text" name="feld" value="%s">', htmlspecialchars($value));

Zu beachten ist allerdings, dass htmlspecialchars() das einfache Anführungszeichen ' normalerweise nicht umschreibt. In dem Fall muss htmlspecialchars() mit ENT_QUOTES als optionalem zweiten Parameter auf den einzufügenden Attributwert angewendet werden.

Beispiel
printf("<input type='text' name='feld' value='%s'>", htmlspecialchars($value, ENT_QUOTES));

Leerzeichen, Tabulatoren und Zeichen für den Zeilenumbruch sind so genannte Whitespace-Zeichen. Im HTML-Kontext werden diese als Leerzeichen dargestellt, wobei mehrere nacheinander auftretende Whitespace-Zeichen zu einem Leerzeichen zusammengefasst werden. Um einen Zeilenumbruch in der Anzeige nicht zu „verlieren“, ist er durch das HTML-Element br darzustellen. Vorgesehen ist dafür die PHP-Funktion nl2br(), die vor jedes NewLine-Zeichen (\n) ein <br> oder <br /> stellt.

In welcher Reihenfolge ist nl2br() und htmlspecialchars() auszuführen? Die von nl2br() erzeugten Zeichen < und > müssen als solche in das HTML-Dokument geschrieben werden und dürfen nicht von htmlspecialchars() nach &lt; und &gt; umgeschrieben werden, damit das br-Element als HTML-Syntax angesehen werden kann und nicht als Datenbestandteil interpretiert wird.

Beispiel
printf('<p>%s</p>', nl2br(htmlspecialchars($value)));

Mit der passenden Aufbereitung der Daten für den HTML-Kontext ist die Aufgabe des PHP-Scripts erledigt. Der Transport zum Empfänger findet außerhalb seines Einflussbereichs statt. Ein Browser wird das HTML-Dokument lesen und korrekt ausgezeichneten Code und Daten regelgerecht zu trennen wissen.

Daten, die ein Browser an den Webserver senden soll, werden vom Browser nicht in einen HTML-Kontext gebracht. Im Normalfall wird man diesen Weg nicht weiter betrachten müssen (solange man nicht händisch eingreift, um beispielsweise mit PHP oder Javascript Daten in eine URL für das action-Attribut eines Formular zu bringen). Ein PHP-Script bekommt die Daten in Rohform über die üblichen Arrays $_GET, $_POST usw. bereitgestellt. Eine Besonderheit stellt die Datenübertragung per Ajax dar.

HTML in der Datenbank

„Vorauseilender Gehorsam“ führt oftmals zu Problemen, wenn sich die Aufgabenstellung ändert oder erweitert wird. Wenn für ein Projekt vorgesehen ist, Daten in einem DBMS zu speichern und diese auf Anforderungen von Clients in ein HTML-Dokument eingefügt auszugeben sind, könnte man auf die Idee kommen, die Daten gleich HTML-gerecht im DBMS abzulegen. Es klingt verlockend, die HTML-Behandlung und die DBMS-Maskierung in einem Schritt erledigen zu können. Dem stehen jedoch gravierende Nachteile gegenüber. Im DBMS kommen keine Rohdaten zu liegen, was eine Stringverarbeitung unmöglich macht. Die für die HTML-gerechte Notation verwendeten zusätzlichen Zeichen sind für ein DBMS ganz normale Zeichen, die unter anderem beim Zählen und Sortieren berücksichtigt werden und falsche Ergebnisse liefern. Hühner hat eine Länge von 6 Zeichen, H&uuml;hner hingegen besteht für das DBMS aus 11 Zeichen. Zudem wird ein & bei einer Sortierung vor den Buchstaben einsortiert, weshalb H&uuml;hner vor Hasen auftauchen werden. Für eine später hinzukommende Ausgabe in einen anderen Kontext als HTML ist die HTML-Aufbereitung ebenfalls hinderlich. Diese Nachteile umgeht man, wenn man die Aufbereitung konsequent zum Zeitpunkt des Kontextwechsels und nur für diesen einen vornimmt. Nebenbei erreicht man durch diese Konsequenz eine Konsistenz im Code: die Behandlung ist genau da zu finden, wo sie benötigt wird.

Anders zu betrachten sind die Anwendungsfälle, die gerade den HTML-Code als im DBMS abzulegende Daten ansehen, wie es zum Beispiel für ein Template-System der Fall sein kann. In Richtung DBMS sind zwar die DBMS-üblichen Maßnahmen zu ergreifen. Eine HTML-gerechte Behandlung beim Einfügen in einen HTML-Kontext darf jedoch nicht stattfinden, denn man möchte ja den HTML-Code von einem Client als Code interpretiert wissen.

Schwierig wird die Sachlage, wenn man Anwendern ermöglichen möchte, eigenen HTML-Code in einen HTML-Kontext einzufügen. Der Möglichkeiten, beispielsweise JavaScript-Code oder CSS-Formatierungen in HTML unterzubringen, gibt es einige und damit auch einige zu beachtende Missbrauchsmöglichkeiten. Ein Patentrezept kann es aufgrund der Vielfalt der möglichen Aufgabenstellungen nicht geben. Hier muss jeder Programmautor selbst gewissenhaft überlegen, was dem Anwender gestattet wird, welche Nachteile das für beide Seiten mit sich bringen kann und nicht zuletzt, wo eine kontextgerechte Behandlung ausgeführt werden muss. Systeme wie BBCode oder Shortcode bei Wordpress können ein Kompromiss sein. Dem Anwender wird damit eine überschaubar geringe Anzahl von Gestaltungsmöglichkeiten gegeben, bei denen nicht alle (Missbrauchs-)Möglichkeiten von HTML zur Verfügung stehen.

URL

Eine URL verwendet diverse Zeichen mit einer Sonderbedeutung – zum Beispiel den / (Schrägstrich) als Pfadtrennzeichen, = (Gleichheitszeichen) und & (Kaufmanns-Und) sowie manchmal auch ; (Semikolon) als Trennzeichen im Querystring. Wie üblich müssen diese Zeichen besonders behandelt werden, damit sie eindeutig als Datenbestandteil angesehen werden können.

Es gibt zwei Stellen in einer URL, die eigentlich[1] unterschiedlich betrachtet werden müssen. Für den Querystring ist urlencode() vorgesehen und für Daten, die im Pfadteil eingefügt werden, gibt es rawurlencode(). Der Unterschied zwischen beiden Funktionen ist lediglich die Behandlung des Leerzeichens. rawurlencode() wandelt es zu %20, urlencode() hingegen in ein + (Plus).

Beispiel
$url = sprintf('http://example.com/resource?name1=%s&name2=%s', urlencode($value1), urlencode($value2));

$url = sprintf('ftp://%s:%s@ftp.example.com/resource', rawurlencode($username), rawurlencode($password));

$url = sprintf('http://example.com/resource/%s', rawurlencode($daten));

Die dritte Zeile findet beispielsweise Anwendung bei „freundlichen URLs“, bei denen die URL teilweise aus variablen Daten gebildet wird, etwa wenn die Überschrift eines Blog-Eintrags in der URL auftauchen soll.

URL in HTML – zweifacher Kontextwechsel

Daten in eine URL zu bringen und diese URL in HTML einzufügen, ist ein Fall, bei dem zwei Kontextwechsel stattfinden.

Beispiel
$url = sprintf('https://example.com/resource?name1=%s&name2=%s', urlencode($value1), urlencode($value2));
$link = sprintf('<a href="%s">Linktext</a>', htmlspecialchars($url));

Zunächst werden die Daten in den URL-Kontext gebracht – in dem Fall mit urlencode(). Die behandelte URL wird anschließend mit htmlspecialchars() für den HTML-Kontext aufbereitet.

Beachten Sie: Jeder Kontextwechsel muss getrennt für sich betrachtet werden. Aufgelöst wird der Knoten „von innen nach außen“. Die für die Maskierung hinzugekommen Zeichen von inneren Kontexten gelten im äußeren Kontext als normales Nutzzeichen, das gemäß den „äußeren“ Regeln behandelt werden muss.

PHP kennt auch eine Funktion zum Erstellen des Querystring aus einem Array oder den öffentlichen Eigenschaften eines Objekts: http_build_query(). Die Funktion kann in dem Fall URL in HTML auch für beide Kontextwechsel arbeiten. Voraussetzung ist, dass die Konfigurationsdirektive arg_separator.output auf &amp; gestellt wurde oder ein &amp; als dritter Parameter übergeben wird.

http_build_query() statt urlencode()
$data = array('name1' => $value1, 'name2' => $value2);
$url = 'https://example.com/resource?' . http_build_query($data);
$link = sprintf('<a href="%s">Linktext</a>', htmlspecialchars($url));
oder:
$data = array('name1' => $value1, 'name2' => $value2);
$url = 'https://example.com/resource?' . http_build_query($data, '', '&amp;');
$link = sprintf('<a href="%s">Linktext</a>', $url);
Natürlich ist auch der Linktext zu beachten. Wenn dieser potentiell HTML-eigene Zeichen enthalten kann, etwa weil er aus Variablen kommt, muss er ebenfalls mit einem htmlspecialchars() behandelt werden.

JavaScript

Um Daten in JavaScript-Code einzufügen, bringt PHP keine Funktion mit. Vor allem String-Begrenzungszeichen und Zeilenumbrüche müssen beachtet werden. Da in JavaScript ein String nicht über mehrere Zeilen gehen darf, ist das folgende Beispiel ungültige Syntax.

Beispiel
var text = 'Eine Zeile Noch eine Zeile';

Für das Zeilenumbruchzeichen ist stattdessen ein \n oder \u000A zu notieren:

Beispiel
var text = 'Eine Zeile\nNoch eine Zeile';

Ein Backslash vor dem (unsichtbaren) Zeilenende fügt kein Zeichen (auch keinen Umbruch) in den String ein, sondern dient nur der besseren Code-Lesbarkeit:

Beispiel
var text = 'Eine Zeile\ Immer noch die selbe Zeile';

Wie erwähnt gibt es keine für JavaScript-Code vorgesehene Maskierfunktion. Das folgende Beispiel zeigt, wie eine solche Funktion aussehen kann.

(PHP-Code)
/**
 * Maskiert Sonderzeichen für den JavaScript-Kontext.
 *
 * @param $value string Der zu behandelnde Wert.
 * @return string
 */
function javascript_escape($value) {
  return strtr((string)$value, array(
    "'" => '\\\'',
    '"' => '\"',
    '\\' => '\\\\',
    "\n" => '\n',
    "\r" => '\r',
    "\t" => '\t',
    chr(12) => '\f',
    chr(11) => '\v',
    chr(8) => '\b',
    '</' => '\u003c\u002F',
    '<script' => '\u003Cscript',
    '<!--' => '<\u0021--',
  ));
}

Alternativ dazu kann die Funktion json_encode() verwendet werden. Diese ist jedoch nicht direkt nur für Strings ausgelegt, sondern wandelt alle PHP-Typen (außer Resource aber inklusive der komplexen Typen Array und Object) in eine JSON-Darstellung. Außerdem maskiert sie nicht nur, sie quotiert (setzt Anführungszeichen um den Wert) gleichzeitig und erwartet UTF-8-kodierte Werte.

Siehe auch die Abschnitte Ajax und Script- und Style-Bereiche im HTML-Dokument.

JavaScript und HTML

Es gibt zu viele Möglichkeiten, Kontexte zu schachteln, deshalb soll hier nur eine als Beispiel gezeigt werden. Als Programmierer muss man sorgfältig beachten, wann welcher Kontext vorliegt. Um zu einer fehlerfreien Implementierung zu kommen, kann es hilfreich sein, sich vorzustellen und vor allem auszuprobieren, wie in den einzufügenden Daten enthaltene Begrenzungszeichen für das empfangende System aussehen. Hierzu kann man das fertig zusammengefügte Ergebnis zur Kontrolle ausgeben. Aber Achtung! Im Browser angezeigt hat dieser bereits den HTML-Kontext interpretiert. Das was der Browser wirklich bekommen hat, sieht man am besten in der Quellcode-Ansicht.

Beispiel
// mit XSS-Injection-Lücke echo "<button onclick=\"tuwas('$eingabewert')\">...</button>";

Der Kontext, in dem dieser Code steht, ist PHP. Für diesen ist erforderlich, dass die Anführungszeichen des onclick-Attributes maskiert werden, womit die Behandlung für den PHP-Kontext erledigt ist. Das unbehandelte $eingabewert stellt jedoch noch eine XSS-Injection-Lücke dar. Derzeit bekommt der Browser das Folgende zu sehen.

Beispiel
<button onclick="tuwas('eingabewert')">...</button>

Der eingabewert steht in einem JavaScript-String-Kontext und der JavaScript-Code steckt in einem HTML-Kontext.

Beispiel
$js = sprintf("tuwas('%s')", javascript_escape($eingabewert)); printf('<button onclick="%s">...</button>', htmlspecialchars($js));

Notiert man die beiden Kontexte (HTML und JavaScript) in einzelne Anweisungen, entspannt sich zum einen die Anführungszeichensituation unter PHP und zum anderen zeigt sich deutlicher die für den jeweiligen Kontext notwendige Behandlung. Allerdings muss dabei weiterhin die Anführungszeichenschachtlung im entstehenden HTML-Code beachtet werden.

Zum Testen, ob die Behandlungen erfolgreich waren, kann $eingabewert mit den kritischen Inhalten gefüllt werden, wobei besonderes Augenmerk auf die Stringbegrenzungszeichen gelegt werden sollte.

Beispiel
$eingabewert = "');alert('test"; // HTML-Quelltextanzeige: <button onclick="tuwas('\');alert(\'test')">...</button> // JavaScript-Code: tuwas('\');alert(\'test') $eingabewert = '"><b>test</b>'; // HTML-Quelltextanzeige: <button onclick="tuwas('\&quot;&gt;&lt;b&gt;test&lt;/b&gt;')">...</button> // JavaScript-Code: tuwas('\"><b>test</b>')

Beide Kontextwechsel wurden jeweils fehlerfrei gemeistert. Der JavaScript-Code wird nicht kompromittiert. Was die Funktion tuwas() damit anstellt, steht auf einem anderen Blatt. In deren Code sind möglicherweise weitere Kontextwechsel zu beachten.

Ajax

Ajax ist eine Technik, die im Browser aus dem Kontext JavaScript heraus ausgeführt wird. Wenn für die Ajax-Requests eine externe Bibliothek verwendet wird, so ist die Übergabe von Werten gemäß deren Richtlinien vorzunehmen. Verwendet man jedoch direkt das XMLHttpRequest-Objekt, so muss man sich um die kontextgerechte Behandlung der Werte selbst kümmern. Zu beachten sind zwei Stellen.

Die Methode open() erwartet unter anderem eine URL. Möchte man in dieser Daten übertragen, so ist der URL-Kontext zu beachten. Die einzufügenden Daten können mit der Funktion encodeURIComponent() behandelt werden. Sie ist sowohl für das Einfügen in den Pfad- als auch in den QueryString-Teil der URL verwendbar.

Im Falle eines POST-Requests nimmt die Methode send() die Daten entgegen. Sie müssen dabei im Normalfall (Content-Type: application/x-www-form-urlencoded) wie der QueryString-Teil einer URL ohne das einleitende Fragezeichen notiert werden. Auch dafür können die einzufügenden Daten mit der Funktion encodeURIComponent() behandelt werden.

Beachten Sie: encodeURIComponent() maskiert Zeichen außerhalb von ASCII als UTF-8-Bytesequenzen.

Script- und Style-Bereiche im HTML-Dokument

Gesetzt den Fall, man hat ein HTML-Dokument, in dem JavaScript-Code eingebettet in einem <script>-Block steht. Es ist in der HTML-Spezifikation definiert, dass in einem solchen alles als Rohtext interpretiert werden muss, auch Markup und Entitys. Lediglich die Sequenz </ hat eine Sonderstellung, denn die beendet den Elementinhalt. Die Browser allerdings richten sich nicht unbedingt nach dieser Vorgabe sondern sind toleranter. In HTML5 ist die gängige Praxis zur Regel erhoben worden und </script> als Script-Ende definiert.

Beachten Sie: Der Inhalt eines Eventhandlers (onclick-Attribute usw.) allerdings wird gemäß den üblichen HTML-Regeln interpretiert.
Beispiel
Ein Ausschnitt aus einem HTML-Dokument:
... <input type="submit" id="foo" /> <div id="bar">Inhalt</div> ...
Vom Submit-Button soll die Beschriftung geändert werden und vom div-Element der Inhalt.
Falsch:
<script type="text/javascript"> 1: document.getElementById('foo').value = '&Auml;ndern'; 2: document.getElementById('bar').innerHTML = '&lt;span style=&quot;color:red&quot;&gt;neuer Inhalt&lt;/span&gt;'; </script>
Zeile 1: Der Submit-Button foo wird nicht Ändern sondern &Auml;ndern anzeigen, da das Entity nicht vom HTML-Parser interpretiert wird.
Zeile 2: Der Browser wird HTML-Code anzeigen, weil auch das Markup ohne Interpretation zum JavaScript durchgereicht wird.
Richtig:
<script> 3: document.getElementById('foo').value = 'Ändern'; 4: document.getElementById('bar').innerHTML = '<span style="color:red">neuer Inhalt<\/span>'; </script>
Zeilen 3 und 4: Beides wird wie gewünscht angezeigt werden. In Zeile 4 wird allerdings der / in </span maskiert, damit der script-Bereich nicht als beendet angesehen wird.
Alternative:
<script type="text/javascript"> 5: document.getElementById('foo').value = '\u00C4ndern'; 6: document.getElementById('bar').innerHTML = '<span style="color:red">neuer Inhalt<\u002Fspan>'; </script>
Zeile 5 verwendet eine Unicode-Sequenz, falls das Ä nicht direkt notiert werden kann. Normalerweise sollte das nicht notwendig sein.
Zeile 6 verwendet die Unicode-Sequenz für den / (Schrägstrich), um dem HTML-Parser das </ zu verstecken. JavaScript interpretiert den Schrägstrich wieder als solchen.

Für <style>-Bereiche für CSS gilt die gleiche Regel bezüglich Markups und Entitys wie für <script>-Bereiche. Die Werte für eine CSS-Eigenschaft werden in den meisten Fällen direkt notiert (z. B. color: red; oder color: #FF0000;). Sollen diese Werte aus einer nicht vertrauenswürdigen Quelle kommen, so muss man andere Wege finden, um ungewollte Werte zu verhindern, denn ein Quotieren oder Maskieren ist hier weder vorgesehen noch möglich. Solche anderen Wege wären beispielsweise ein Abgleich gegen eine Liste von erlaubten Werten, ein Test auf Einhaltung der Syntax-Regeln oder eine anderweitig geeignete Prüfung.

E-Mail

Eine E-Mail besteht im einfachsten Fall aus Kopf-Zeilen (Header) und dem Inhalt (Body). Header-Zeilen transportieren Metadaten zur Mail, zum Beispiel Absender- und Empfänger-Angabe (From, To), den Betreff (Subject) oder Angaben zum Inhalt (Content-Type). Jede Header-Zeile endet mit einem Zeilenumbruchszeichen[2]. Header und Body sind durch eine Leerzeile voneinander getrennt – oder anders gesagt: die letzte Header-Zeile endet mit zwei Zeilenumbruchszeichen. Beim Einfügen von Werten in Headerzeilen ist nun zu beachten, dass darin keine Zeilenumbruchszeichen enthalten sind, sonst können beliebige Headerzeilen hinzugefügt werden. Die Funktion mail() nimmt die drei Pflichtparameter to, subject und message entgegen sowie zwei optionale Parameter, von denen der erste namens additional_headers interessant ist, weil über ihn weitere Headerzeilen eingefügt werden können. Die Werte für to und subject werden von PHP so behandelt, dass alle enthaltenen Steuerzeichen inklusive Zeilenumbruch durch Leerzeichen ersetzt werden. Sie sind also schon seitens PHP gegen E-Mail-Header-Injection gesichert. Über additional_headers werden oftmals Angaben wie From, Cc und Bcc hinzugefügt. Dafür werden keine Steuerzeichen benötigt, vor allem keine Zeilenumbruchszeichen. Sind solche in den Eingabewerten enthalten, ist das ein recht sicheres Zeichen für einen Missbrauchsversuchs. Es lohnt sich nicht, sie gegen Leerzeichen auszutauschen, denn den Missbrauch wird man lieber generell verhindern wollen. Ein kommentarloser Script-Abbruch wäre also auch angemessen. Eine eventuell ausgegebene Fehlermeldung wird ein automatischer Angreifer nicht auswerten und der normale Anwender nicht zu sehen bekommen, weil er mit harmlosen Daten den Abbruch nicht auslöst.

Beispiel
/**
 * Ermittelt, ob ein String nicht druckbare Zeichen enthält. 
 * 
 * @param $subject der zu testende String
 * @param $utf8 optional - true, wenn der Teststring UTF-8-kodiert ist.
 * @return boolean
 */
function is_printable($value, $utf8 = false) {

  $pattern = '~^\P{C}+\z~' . ($utf8 ? 'u' : '');
  return (bool)preg_match($pattern, $value); 
}

if (!is_printable($from) or !is_printable($cc))
  // $from oder $cc enthält nicht nur druckbare Zeichen
  die(); // Abbruch wegen potentiellen Missbrauchs

$headers = array();
$headers[] = 'Content-Type: text/plain';
$headers[] = 'From: ' . $from;
$headers[] = 'Cc: ' . $cc;
mail($to, $subject, $message, implode("\n", $headers));

In diesem Beispiel werden Angaben zum Absender und Kopie-Empfänger als optionale Header-Zeilen und eine Inhaltstypangabe eingefügt, wobei Werte für From und Cc aus einer externen Quelle kommen. Diese werden mit der Funktion is_printable() getestet, ob sie nur druckbare Zeichen enthalten. Wenn dies nicht der Fall ist, bricht das Script ab. Ansonsten werden die einzelnen Headerzeilen zunächst als je ein Element des Arrays $headers angelegt. Beim Aufruf der Funktion mail() wird aus diesem Array ein Header-Zeilen-String erstellt, dessen einzelne Angaben mit je einem Zeilenumbruchszeichen (\n) getrennt sind.

Für alle Header-Zeilen gilt, dass sie aus historischen Gründen nur 7-Bit-Zeichen enthalten dürfen. Alle Nicht-ASCII-Zeichen (darunter fallen unter anderem die Umlaute) müssen daher umkodiert werden. Ist die Multibyte-String-Extension verfügbar, kann die Funktion mb_encode_mimeheader() dafür verwendet werden.

Beispiel
mb_internal_encoding('utf8'); // andere Werte z.&nbsp;B. ISO-8859-1 oder Latin1

// $subject = 'Umlautärger';
$email_subject = mb_encode_mimeheader($subject, 'utf8', 'Q');
// $email_subject enthält nun =?UTF-8?Q?Umlaut=C3=A4rger?=

$email_from = sprintf('"%s" <%s>', mb_encode_mimeheader($from_name, 'utf8', 'Q'), $from_email);

Zuerst wird der MB-Extension mit der Funktion mb_internal_encoding() mitgeteilt, in welcher Codierung die zu verarbeitenden Strings vorliegen, was in diesem Beispiel UTF-8 sein soll. Der zweite Parameter von mb_encode_mimeheader() gibt die gewünschte Codierung des Ergebnisses an. Die Subject-Zeile muss komplett behandelt werden. Für eine E-Mail-Adresse hingegen (im Beispiel $email_from) darf nur der optionale Name, nicht jedoch die Adresse selbst kodiert werden.

Was noch übrig bleibt ist der Nachrichtentext. Der ist bei reinem Text (plain text) unkritisch. Bei anderen Inhaltstypen (beispielsweise HTML) sind die üblichen kontextspezifischen Regeln zu beachten, wenn dort Werte eingefügt werden sollen.

Dateiformate, stellvertretend CSV

Für die vielfältigen Regeln der einzelnen Dateiformate soll hier nur stellvertretend das Format CSV (Comma/Character Separated Values) genannt werden. Ein allgemein verbindlicher Standard existiert nicht, es ist nur in RFC 4180 beschrieben. Für die Trennung von Datensätzen wird meist ein Zeilenumbruch verwendet, für die Felder finden Zeichen wie Komma, Semikolon, Doppelpunkt, Tabulator oder andere Verwendung. Welche Trennzeichen auch immer verwendet werden, es bleibt das übliche Problem, dass diese Zeichen auch in den Daten vorkommen können. Datenfelder müssen dann begrenzt werden. Als Folge ist auch das Begrenzungszeichen als Sonderzeichen zu beachten.

Für das Lesen und Schreiben von CSV-Dateien stellt PHP die Funktionen fgetcsv() und fputcsv() zur Verfügung. Außerdem gibt es die Funktion str_getcsv() für in Strings vorliegende CSV-Daten.

Für andere Dateiformate muss im Einzelfall geprüft werden, welche Besonderheiten zu beachten sind. Wenn es nur darum geht, mehrere Daten in einer Textdatei abzulegen, so kann man die Daten in eine Struktur bringen (Array oder Objekt) und diese mit serialize() in eine Textform bringen. Die Funktion unserialize() erzeugt dann wieder die originale Form.

Programmausführung über Shell-Kommandos

Gerade hier ist es wichtig, Nutzdaten richtig zu behandeln, um eine ungewollte Programmausführung oder Schlimmeres zu verhindern. Da dies ein Thema für Fortgeschrittene ist, sei an dieser Stelle nur kurz auf die beiden PHP-Funktionen escapeshellarg() und escapeshellcmd() verwiesen. Anwendungsbeispiele können dem PHP-Handbuch entnommen werden.

Reguläre Ausdrücke

Relativ selten wird man einen regulären Ausdruck aus variablen Bestandteilen zusammensetzen. Falls doch, gibt es dafür in PHP für PCRE die Funktion preg_quote().

Fußnoten

  1. Eine URL-Dekodierfunktion wird höchstwahrscheinlich alle %XX-Sequenzen inklusive %20 richtig dekodieren, auch wenn der Kontext eigentlich ein + vorschreibt. Insofern könnte man generell zu rawurlencode() greifen. Entscheiden Sie selbst, ob Sie korrekterweise die Anwendungsfälle unterscheiden wollen oder das (recht geringe) Restrisiko der %20-Nichterkennung ignorieren. JavaScript kennt beispielsweise auch keine Funktion, die das Leerzeichen wie urlencode() behandelt, nur die rawurlencode()-Arbeitsweise findet man in den URL/URI-Kodierfunktionen.
  2. Es gibt auch mehrzeilige Header-Zeilen, doch mit solchen kommt man nur selten direkt in Berührung.