Artikel:Kontextwechsel

Aus SELFHTML-Wiki
Wechseln zu: Navigation, Suche

Dieser Artikel befasst sich mit der Problematik des Kontextwechsels. „Problematik“ deshalb, weil die Nichtbeachtung des Kontextwechsels eine der häufigsten Quellen für verschiedene Probleme ist. Ziel ist es, die häufigsten Kontextwechsel im Web-Umfeld aufzuzeigen und wie diese behandelt werden. Das soll vorwiegend mit Beispielen für PHP und MySQL und textbasierenden Daten erklärt werden. Prinzipiell betrifft das Thema jedoch auch andere Sprachen und andere Datentypen.

Der Artikel bezieht sich auf das Zeichen als kleinste Einheit. Auf welche Weisen Zeichen im Computer durch Bits und Bytes dargestellt werden, soll hier nicht weiter thematisiert werden. Dazu sei auf die Seite Zeichenkodierung und geschriebene Sprache verwiesen. Unterschiedliche Zeichenkodierungen beim Datenaustausch zwischen mehreren Systemen können zwar ebenfalls als Kontextwechsel angesehen werden, doch diese Thematik und deren Probleme ist zu umfangreich, um sie auch noch in diesem Artikel unterzubringen.

Die in diesem Artikel enthaltenen Links zum PHP-Handbuch verweisen generell auf die englische Dokumentation, da dies das Original ist und die Übersetzungen mitunter nicht aktuell genug oder nicht vollständig sind. Im Kopfteil jeder Handbuchseite kann allerdings bei Bedarf und „auf eigenes Risiko“ eine andere Sprache gewählt werden.

Inhaltsverzeichnis

[Bearbeiten] Einleitung

Im Anfang war das Wort … und kurz darauf bekam ein Informatiker die Aufgabe dieses Wort mit Hilfe eines Rechners zu verarbeiten. Er musste dem Computer Anweisungen zum Verarbeiten des Wortes geben, aber auch das Wort selbst musste irgendwie in den Arbeitsspeicher gelangen. Er stand vor dem Problem, Programmcode und das Wort in ein Dokument zu bringen, so dass der Computer dieses Dokument lesen und beides eindeutig voneinander unterscheiden kann. In der Literatur tritt ein vergleichbarer Fall auf, wenn wörtliche Rede in eine Erzählung eingefügt werden soll. Man schließt dabei das Gesagte in Anführungszeichen ein. Auch in den Programmiersprachen hat man meist diese Vorgehensweise übernommen.

[Bearbeiten] Programmcode und Daten

Beispiel
Der Computer sagte: "Meine Batterie ist alle." Dann schaltete er sich selbst aus.
echo "Meine Batterie ist alle."; shutdown();

Das erste Beispiel beginnt zunächst im Erzählmodus. Das erste Anführungszeichen wechselt zur wörtlichen Rede, das zweite beendet diese und die Erzählung wird fortgesetzt.

Auch das zweite Beispiel ist auf diese Weise aufgebaut. Der Code-Parser liest die Zeichen und versucht sie gemäß seiner Syntaxregeln zu interpretieren. Code und Daten verwenden jedoch zu großen Teilen die gleichen Zeichen (Buchstaben und Satzzeichen). Wann aber gehört ein Zeichen zur Syntax und wann zu den Daten? Das erste Anführungszeichen leitet einen Kontextwechsel ein. Alle folgenden Zeichen werden nun als Daten gelesen und nicht als Code interpretiert. Das zweite Anführungszeichen kennzeichnet das Ende des Datenkontextes. Der Rest wird wieder als Code interpretiert.

Dieses Beispiel ist eindeutig interpretierbar. Anders sieht die Sachlage aus, wenn ein " (Anführungszeichen) in den Daten enthalten ist.

Beispiel
$text = "Der Computer sagte: "Meine Batterie ist alle." Dann schaltete er sich selbst aus.";

Das Anführungszeichen hat bereits die Bedeutung des Kontextwechsels zwischen Code und Daten. Das zweite Anführungszeichen vor dem Wort Meine würde nun den Datenkontext beenden. Die nachfolgenden Zeichen sind früher oder später nicht mehr als gültige PHP-Syntax interpretierbar. Es kommt zu einem Syntax-Fehler.

Abhilfe ist die kontextgerechte Behandlung des Anführungszeichen. Die PHP-Syntax-Regel[1] besagt: Wenn ein Anführungszeichen in einem mit Anführungszeichen eingefassten String vorkommt, muss ihm ein \ (Backslash) vorangestellt werden.

Beispiel
$text = "Der Computer sagte: \"Meine Batterie ist alle.\" Dann schaltete er sich selbst aus.";

Der Backslash maskiert das zweite Anführungszeichen. Es wird nun nicht mehr als Stringbegrenzer angesehen. Durch die Verwendung des Backslashs als Maskierzeichen hat dieser nun ebenfalls eine Sonderbedeutung bekommen und ist selbst zu maskieren, wenn er als Datenbestandteil vorkommt. Dies geschieht, indem man ihm einen weiteren Backslash voranstellt.

Maskierungen (englisch Escaping) sind nur für die richtige Interpretation von Zeichen beim Lesen eines Dokuments oder Datenstroms notwendig. Die interne Verarbeitung wird meistens mit den Rohdaten stattfinden, denn Maskierzeichen sind meist auch nur normale Zeichen und dürfen bei der Stringverarbeitung (zum Beispiel Zeichen zählen) nicht berücksichtigt werden. Die Maskierzeichen werden beim Lesevorgang entfernt. Im Speicher steht der Wert also wieder als:

Beispiel
Der Computer sagte: "Meine Batterie ist alle." Dann schaltete er sich selbst aus.

[Bearbeiten] Daten ausgeben

Ebenso wie beim Einlesen von Daten ist bei der Ausgabe ein möglicher Kontextwechsel zu beachten. Denn die Ausgabedaten des einen Systems sind nicht selten die Eingabedaten eines anderen Systems. Auf einem textbasierenden Bildschirm muss einfach nur eine in Rohform vorliegende Zeichenkette Zeichen für Zeichen dargestellt werden. Um den String jedoch beispielsweise in einem SQL-basierenden Datenbanksystem abzulegen, muss er in ein SQL-Statement eingebettet werden, weil auch SQL vorsieht, Anweisung und Daten als eine gemeinsame Zeichenfolge zu notieren. Zu beachten sind hier wiederum die Zeichen, die im Datenkontext eine Sonderbedeutung haben, insbesondere die Begrenzungszeichen.

[Bearbeiten] Alternative Betrachtungsweise: Literale

Ein Literal ist eine Zeichenfolge, die einen bestimmten Wert repräsentiert. Möchte man zum Beispiel im Programmcode einen Zahlenwert angeben, so kann man ihn nicht direkt als Bytewert(e) oder ähnliches einfügen, sondern muss sein Literal verwenden. Diese setzt sich aus den einzelnen Ziffern zusammen, für die das jeweilige Zeichen notiert wird. Um eine 42 zu notieren, notiert man das Zeichen 4 gefolgt vom Zeichen 2. true und false wären die Literale für die beiden booleschen Werte. Literale von Text notiert man in vielen Programmiersprachen innerhalb von Anführungszeichen. Das Literal von SELFHTML wäre demnach "SELFHTML". Das Literal eines " (Anführungszeichens) aber ist "\"", und von Bei"spiel"text wäre es "Bei\"spiel\"text". Da die Anführungszeichen zur Begrenzung des Literals dienen, müssen sie, wenn sie als Teil eines Wertes vorkommen, durch ein \" repräsentiert werden.

Jede Progammiersprache – oder allgemeiner gesagt: jeder Kontext – definiert mehr oder weniger eigene Regeln, wie die Literale von bestimmten Werten aussehen müssen. Möchte man einen Wert notieren, so muss man im Prinzip nur das zugehörige Literal gemäß den Regeln des jeweiligen Kontextes bilden.

[Bearbeiten] Fehler und deren Auswirkungen

Im PHP-Code wird eine Nichtbeachtung des Kontextwechsels recht schnell durch einen Syntax-Fehler auffallen.

Beispiel
$sql = "SELECT feldliste FROM tabelle WHERE feld='$variable'";

In diesem Beispiel kommen zwei geschachtelte Kontextwechsel vor. Mit PHP-Code wird einen String in einer Variable abgelegt, der im weiteren Verlauf als MySQL-Statement verwendet werden soll. Der innere Kontext ist dabei der interessantere. Wenn zur Laufzeit in $variable der Wert Beispiel steht, so ergibt sich ein SQL-Statement

Beispiel
SELECT feldliste FROM tabelle WHERE feld='Beispiel'

Steht allerdings Beispiel mit ' drin darin, so ergibt sich

Beispiel
SELECT feldliste FROM tabelle WHERE feld='Beispiel mit ' drin'

Richtig hingegen wäre

Beispiel
SELECT feldliste FROM tabelle WHERE feld='Beispiel mit \' drin'

Auch MySQL verwendet den Backslash als Maskierungszeichen.[2]

Unmaskiert ist der String „nur“ syntaktisch inkorrekt. MySQL wird sich mit einer Fehlermeldung darüber beklagen. Gelingt es hingegen jemandem, $variable so zu füllen, dass das SQL-Statement am Ende wie folgt aussieht, so kann er beispielsweise an die Daten der Benutzertabelle kommen.

Beispiel
SELECT feldliste FROM tabelle WHERE feld='Beispiel mit ' UNION SELECT feldliste FROM userdaten -- drin'

Somit hätte das PHP-Script eine SQL-Injection-Lücke.

Beim folgenden Beispiel gelingt das Anmelden als Administrator ohne dessen Passwort zu kennen.

Beispiel
$sql = "SELECT feldliste FROM users WHERE username='$username' AND password='$password'";
Als Benutzername wurde „admin' -- “ eingegeben. Das fertige SQL-Statement sieht nun so aus:
SELECT feldliste FROM users WHERE username='admin' -- ' AND password=''
Die Sequenz Minus-Minus-Leerzeichen leitet einen Kommentar ein, so dass die zweite Bedingung nicht mehr zur Auswertung kommt.
Beachten Sie: Das Risiko eines Fehlers oder SQL-Injection besteht nicht nur bei Daten die mehr oder weniger direkt aus Benutzereingaben stammen. Einem SQL-Statement ist es egal, wie und aus welchen Quellen es entstanden ist. Am Ende muss sich eine gültige Syntax ergeben und die erzeugte Anweisung muss auch der Intention des Autors entsprechen. Der Kontextwechsel muss also in jedem Fall angemessen berücksichtigt werden.

[Bearbeiten] Verhindernde Maßnahmen

Für den Kontextwechsel aus dem eben gezeigten Beispiel – String in ein MySQL-Statement einfügen – müsste man sämtliche relevante Syntax-Regeln MySQLs berücksichtigen und vor dem Einfügen des Variableninhalts selbigen nach Zeichen mit Sonderbedeutung (Anführungszeichen und einige andere) durchsuchen und diese Zeichen durch ihr maskiertes Pendant austauschen. Diesen Aufwand kann man sich sparen, denn PHP stellt eine Funktion bereit, die diese Arbeit übernimmt: mysql_real_escape_string() oder mysqli_real_escape_string() je nach verwendeter PHP-Extension – mysql oder mysqli.[3]

Beispiel
$sql = "SELECT feldliste FROM tabelle WHERE feld='" . mysql_real_escape_string($variable) . "'";

Damit man nicht immer den String für das Einfügen der Maskierfunktion verlassen muss, kann man die Platzhalter-Funktion sprintf() hinzunehmen.

Beispiel
$sql = sprintf("SELECT feldliste FROM tabelle WHERE feld1='%s' AND feld2='%s'", mysql_real_escape_string($variable1), mysql_real_escape_string($variable2));

Der einfachste Anwendungsfall von sprintf() verwendet %s als Platzhalterzeichen für einen String. Pro Platzhalter ist der Funktion ein weiterer Parameter zu übergeben, der an dessen Stelle eingefügt wird.

[Bearbeiten] Besonderheiten: Identifier, ORDER BY

Identifier (Bezeichner) sind die Namen für Datenbanken, Tabellen und Felder. Diese werden meist direkt notiert, sprich: sowohl als fester Statementbestandteil als auch ohne Begrenzungszeichen. Es gibt Situationen, in denen man nicht auf die Begrenzungszeichen verzichten kann oder will oder in denen der Identifier aus Daten stammend eingebaut werden soll. Für MySQL müssen dann die Identifier mit ` (Backticks) eingefasst werden[4]. Für Identifier gibt es nur eine eigene Regel aber keine vorgefertigte PHP-Funktion: Ein im Identifier enthaltener Backtick ist doppelt zu notieren.

Will man die Ausgabe einer Ergebnismenge vom Anwender sortieren lassen, muss man das Sortierfeld und die Richtung in das SQL-Statement einarbeiten. Der Feld- oder Aliasname ist ein Identifier und könnte nach der Identifier-Regel behandelt werden. Gibt allerdings jemand einen Namen eines nicht vorhandenen Feldes an, so gibt es aber trotzdem eine Fehlermeldung von MySQL. Besser ist es, den übergebenen Wert gegen eine Liste der erlaubten Sortierfeldnamen zu prüfen.

Die Sortierrichtungsangaben ASC und DESC sind Bestandteil der SQL-Syntax, also Code, der ohne Kontextwechsel eingefügt werden muss. Eine SQL-Injection-Lücke verhindert man hier ebenfalls nur mit einer Prüfung.

Beispiel
$sql = "SELECT ... FROM ... WHERE ..."; $orderNames = array('feld1', 'feld2', 'alias1'); if (in_array($_GET['orderBy'], $orderNames)) { $orderDir = (isset($_GET['orderDir']) and $_GET['orderDir'] == 'DESC') ? 'DESC' : 'ASC'; $sql .= sprintf(" ORDER BY `%s` %s", str_replace('`', '``', $_GET['orderBy']), $orderDir); }

Das im Vorfeld in $sql aufgebaute SQL-Statment hat noch keine ORDER-BY-Klausel. Der GET-Parameter orderBy wird auf Vorhandensein im Array $orderNames geprüft. Ist das der Fall wird orderDir definiert auf DESC oder ASC gesetzt. Anschließend wird die ORDER-BY-Klausel an das SQL-Statement angehängt. Ein ungültiger Richtungswert ergibt ASC, ein ungültiger Feldname führt in diesem Beispiel zum kompletten Weglassen der ORDER-BY-Klausel.

[Bearbeiten] Besonderheit: der LIKE-Operator

Der LIKE-Operator gibt den beiden Zeichen % (Prozent) und _ (Unterstrich) eine Sonderbedeutung. Möchte man nicht, dass ein Anwender diese Zeichen als Platzhalter (wildcard character) in einem Suchmuster verwenden kann, so müssen diese mit einem vorangestellten \ (Backslash) maskiert werden. MySQL gestattet aber auch, ein beliebiges anderes Maskierzeichen (escape character) zu definieren.

Mit dem folgenden Beispiel soll dieses SQL-Statement gebildet werden:

Beispiel
SELECT ... FROM ... WHERE feld LIKE 'searchFor%'

Anstelle von searchFor soll eine Benutzereingabe eingefügt werden. Dabei sollen eventuell eingegebene % und _ genau so im Suchergebnis enthalten sein – denn als alleiniges Jokerzeichen soll das dem searchFor folgende % Verwendung finden.

Beispiel
$replacePairs = array('%' => '\%', '_' => '\_'); $sql = sprintf("SELECT ... FROM ... WHERE feld LIKE '%s%%'", strtr(mysql_real_escape_string($searchFor), $replacePairs));

Die Funktion sprintf() nimmt mit %s einen Platzhalter entgegen. An seiner Stelle wird das Ergebnis des nächsten Funktionsparameters eingefügt. Das nach dem Platzhalter folgende %% steht aufgrund des Kontextes „sprintf()-Format-String“ für ein % (Prozentzeichen) – welches als Jokerzeichen für den LIKE-Operator vorgesehen ist.

Der zu suchende Wert steht in der Variable $searchFor. Er wird zunächst mit mysql_real_escape_string() allgemein für den String-Kontext innerhalb eines SQL-Statements vorbereitet. Die Zeichen % und _ werden dabei nicht beeinflusst. Sie werden anschließend mit der Funktion strtr() durch ihre maskierten Formen gemäß dem Mustervergleichskontext (pattern-matching context) des LIKE-Operators ausgetauscht. Dazu wurden vorher in $replacePairs die nötigen Zuordnungen erstellt.

Angenommen in $searchFor steht „25% von“, so ergibt sich nun dieses SQL-Statement:

Beispiel
SELECT ... FROM ... WHERE feld LIKE '25\% von%'

[Bearbeiten] Alternative: Prepared Statement

Bei einem Prepared Statement schreibt man ähnlich wie bei der bereits erwähnten Funktion sprintf() zunächst das SQL-Statement mit Platzhaltern. Dieses SQL-Statement gelangt in einem Prepare (vorbereiten) genannten Schritt zum SQL-Server. An die Platzhalter werden Variablen gebunden. Mit einem Execute (ausführen) werden dann die Daten aus den Variablen an den SQL-Server gesendet. In diesem Fall werden die Daten nicht in den SQL-Statement-Kontext gebracht, denn das SQL-Statement und die Daten gehen getrennte Wege zum DBMS. Demzufolge müssen und dürfen sie auch nicht dafür aufbereitet werden. Prepared Statements sind also schon vom Prinzip her unanfällig gegen fehlerhafte Maskierungen und SQL-Injection. Dem Nicht-Behandeln-Müssen der Daten steht auf der anderen Seite der etwas erhöhte Programmieraufwand gegenüber. Prepared Statements sind eigentlich dafür vorgesehen, ein Statement mit unterschiedlichen Daten mehrfach nutzen zu können. Dabei fällt das mehrfache Parsen des SQL-Statements weg. Wenn jedoch – wie bei PHP üblich – jeder Request eine eigene Datenbankverbindung aufbaut, ist dieser Vorteil nur dann gegeben, wenn während der Abarbeitung des Requests mehrere gleiche Statements aber mit unterschiedlichen Daten abgearbeitet werden sollen, wie es beim Verarbeiten von Massendaten der Fall ist. Der Vorteil der SQL-Injection-Immunität hingegen bleibt. Zusätzlich müssen die Besonderheiten von Identifiern und dem LIKE-Operator berücksichtigt werden, denn diese sind vom Prepared-Statements-Mechanismus nicht betroffen.

Beispiele zu Prepared Statements können den PHP-Handbuch-Kapiteln zu mysqli und PDO entnommen werden (siehe zum Beispiel bei mysqli_prepare() beziehungsweise PDO: Prepared statements and stored procedures).

[Bearbeiten] Zahlen im (My)SQL-Statement

Beachten Sie: Ein falsch behandelter Kontextwechsel kann ebenso fatale Folgen haben wie ein nicht behandelter. Zumindest kann es zu falschen Ergebnissen führen, wenn man für den einen Kontext maskiert, die Daten aber tatsächlich in einem anderen bringt.
Beispiel
SELECT feldliste FROM tabelle WHERE feld=42

Die 42 ist als Zahlenwert notiert. Dem Vergleichsoperator = folgt kein Anführungszeichen und somit wurde kein String-Kontext eingeleitet.

Wenn statt der 42 beispielsweise ein aus einer Nutzereingabe stammender Wert eingefügt werden soll (23 UNION SELECT …), so wird dieser sofort als Befehlsbestandteil angesehen. Die Funktion mysql_real_escape_string() hilft an dieser Stelle nicht, denn sie maskiert nur und quotiert nicht.

Beispiel
$sql = sprintf('SELECT feldliste FROM tabelle WHERE feld=%s', intval($zahl));

Die Funktion intval() liefert garantiert eine Integerzahl zurück. Wenn $zahl keine gültige Stringdarstellung einer Zahl ist, so ist das Ergebnis zumindest 0. Für Fließkommazahlen gibt es floatval(). Auch ein Typecast nach int oder float garantiert einen Zahlenwert.

Alternativ kann (zumindest unter MySQL) die einzufügende Zahl auch in String-Begrenzern notiert und mit mysql_real_escape_string() behandelt werden. Der Typecast wird dann von MySQL vorgenommen. (MySQL muss sowieso einen String in eine Zahl umwandeln, denn auch ohne Anführungszeichen ist eine Zahl in einem SQL-Statement-String in ihrer String-Darstellung notiert.)

[Bearbeiten] PHP-Besonderheit Magic Quotes

Um SQL-Injection zu verhindern, wurde das PHP-Feature Magic Quotes entwickelt. Wenn es aktiviert ist, werden alle Eingabedaten (GET, POST und Cookies) vor dem Scriptstart von PHP mit der Funktion addslashes() behandelt. addslashes() behandelt zwar weniger Zeichen als mysql_real_escape_string(), doch es sind alle für (My)SQL-Injection kritischen Zeichen abgedeckt.

Oftmals hat dieses Feature jedoch mehr negative Nebenwirkungen als Nutzen. Das Problematische an ihm ist, dass es auf die Eingabedaten wirkt – also nicht kontextgerecht sondern pauschal. PHP kann vor dem Scriptstart noch nicht wissen, welchen Weg die Daten tatsächlich gehen werden. Werden die Daten in eine (My)SQL-Datenbank geschrieben, so sind sie zwar für den notwendigen Kontextwechsel vorbereitet. Müssen sie jedoch in HTML ausgegeben werden – sei es zusätzlich zum Datenbankeintrag als Kontrollausgabe oder weil sie fehlerhaft waren und dem Anwender erneut zur Korrektur vorgelegt werden müssen – so ist die SQL-Kontext-Behandlung falsch und stattdessen eine HTML-Kontext-Behandlung notwendig. Man kann zwar wegen der Vorbehandlung den SQL-Kontextwechsel ignorieren, muss aber nun zusätzlich zu allen anderen Kontextwechseln beachten, dass die Daten nicht in Rohform vorliegen und zunächst mit stripslashes() behandelt werden müssen. Empfehlenswert ist es deshalb, das Feature Magic Quotes komplett zu deaktivieren oder – wenn das zum Beispiel beim Hoster nicht geht – seine Auswirkungen rückgängig zu machen. Beides ist im PHP-Handbuch-Kapitel Disabling Magic Quotes beschrieben.

Magic Quotes ist ein mittlerweile missbilligtes (deprecated) Feature und wird in PHP 6 nicht mehr verfügbar sein. Spätestens dann muss man sich vollständig selbst um seine Kontextwechsel kümmern.

[Bearbeiten] Fußnoten

  1. Die genauen Syntax-Regeln können dem PHP-Handbuch-Kapitel zu Strings entnommen werden.
  2. Es ist alternativ möglich, das als Begrenzungszeichen genutzte Anführungszeichen doppelt zu notieren: 'Beispiel mit '' drin'. Die üblichere Vorgehensweise ist jedoch der vorangestellte Backslash. Auch wenn man für die Anführungszeichen die Verdopplungsmethode verwendet, muss der Backslash selbst immer noch durch Verdopplung maskiert werden.
  3. Bei einigen im asiatischen Raum verbreiten Zeichenkodierungen muss diese unbedingt mittels mysql(i)_set_charset() eingestellt werden, sonst kann mysql(i)_real_escape_string() nicht richtig arbeiten, zerstört einige Zeichen und hinterlässt unter Umständen eine Injection-Lücke. Ein SET NAMES-Statement ist ebenfalls nicht geeignet, da es mysql(i)_real_escape_string() nicht beinflusst. Für die Kodierungen der ISO-8859-Familie und UTF-8, die unter anderem im deutschsprachigen Raum verbreitet sind, ist mysql(i)_set_charset() nicht erforderlich – jedenfalls nicht wegen sicherheitstechnischer Aspekte.
  4. Es gibt einen ANSI-Kompatibilitäts-Modus (Stichwort: ANSI_QUOTES), in dem werden String-Literale nur in einfachen Anführungszeichen notiert und doppelte Anführungszeichen stehen für Identifier.

[Bearbeiten] Fortsetzung

Die Fortsetzung geht auf verschiedene konkrete Kontextwechsel-Situationen ein – unter anderem: SQL, HTML, URL, JavaScript, E-Mail.

Meine Werkzeuge
Namensräume

Varianten
Aktionen
Übersicht
Hilfe
SELFHTML
Diverses
Werkzeuge
Flattr
Soziale Netzwerke