Schutz vor "SQL Injection": So bleibt Ihre APEX-Anwendung sicher!

SQL Injection ist ein vieldiskutierter Begriff im Bereich der Web-Anwendungen. Mit SQL Injection verändert ein Angreifer die SQL-Kommandos Ihrer Anwendung - er injiziert eigene SQL-Anweisungen und verändert damit das Verhalten der Anwendung. Ist eine Anwendung für SQL Injection anfällig, so könnten Sicherheitsregeln ausgehebelt werden. 

SQL Injection ist von der verwendeten Technologie unabhängig. So können alle Web-Anwendungen anfällig sein, egal ob sie mit Java, PHP, Application Express oder anderen Technologien erstellt wurden. Das Problem entsteht, wenn SQL-Anweisungen in der Anwendung dynamisch durch Stringverkettung zusammengesetzt werden. Bei Web-Anwendungen kommt dazu, dass Variableninhalte vielfach durch URL-Manipulationen verändert werden können.

Die gute Nachricht zuerst: Oracle Application Express bietet Ihnen einige Möglichkeiten, sich vor SQL Injection zu schützen. So sind Komponenten, die Sie mit den Application Express-Assistenten erstellt haben,  nicht anfällig für SQL Injection. Wenn Sie also ein Formular auf eine Tabelle oder auf eine Stored Procedure  mit den Application Express-Assistenten erstellen, so sind Sie auf der sicheren Seite. Wenn Sie Berichte mit "eigenen" SQL-Abfragen oder PL/SQL-Prozesse hinterlegen, sind diese zwar grundsätzlich anfällig - es gibt jedoch auch hier zahlreiche Möglichkeiten, die Anwendungen zu schützen. Im heutigen Tipp erfahren Sie, wann Ihre Anwendungen anfällig sind, und wie Sie in diesen Fällen SQL Injection verhindern.

Zusätzlichen Schutz bringt die Oracle Virtual Private Database, welche vor einiger Zeit in diesem Forum vorgestellt wurde (Mandantenfähige Anwendungen). In diesem Fall werden Zugriffsregeln direkt an der Datenbanktabelle hinterlegt - ein SQL Injection-Angriff, welcher darauf zielt, alle Zeilen einer Tabelle zu sehen, geht dann ins Leere.

Zunächst wollen wir eine Application Express-Anwendung erzeugen, die für SQL Injection anfällig ist. Erstellen Sie ein einfaches Szenario anhand der Tabellen EMP und DEPT. Hinterlegen Sie zwei Berichte - einen auf die Tabelle DEPT und einen auf die Tabelle EMP. Verwenden Sie für den Bericht auf EMP die untenstehende SQL-Abfrage.

select * from EMP where deptno = '&P1_DEPTNO.'

Die meisten von Ihnen würden diese Abfrage wahrscheinlich nicht so formulieren - tun Sie es trotzdem. Wir kommen weiter hinten zur "richtigen" Vorgehensweise. Legen Sie anschließend noch ein ausgeblendetes Element P1_DEPTNO und erzeugen Sie im Bericht auf DEPT einen Link (Abbildung 1), so dass der Bericht auf EMP die Zeilen der Abteilung anzeigt, welche vorher im Bericht auf  DEPT angeklickt wurde.

Einrichten des Hyperlinks im Bericht auf DEPT
Abbildung 1: Einrichten des Hyperlinks im Bericht auf DEPT

Ihre Seite sollte dann wie folgt aussehen:

Ausgangssituation: Zwei verlinkte Berichte
Abbildung 2: Ausgangssituation: Zwei verlinkte Berichte

Klicken Sie nun die Hyperlinks im Bericht auf DEPT an - sieht soweit alles ganz gut aus, oder ...?

Ändern Sie nun die URL oben von ...

 /pls/apex/f?p=[APP_ID]:[PAGE_ID]:[SESSION]::NO::P1_DEPTNO:10

... in ....

 /pls/apex/f?p=[APP_ID]:[PAGE_ID]:[SESSION]::NO::P1_DEPTNO:10' or 1=1 --

Nun sehen Sie alle Zeilen ... es geht aber noch weiter: Wenn Sie auf diese Weise ein UNION ALL übergeben, können Sie jede beliebige Tabelle anzeigen - man könnte beispielsweise abfragen, welche Datenbankobjekte vorhanden sind - das Ergebnis würde so aussehen.

Abfrage der Data Dictionary View USER_OBJECTS mit SQL Injection
Abbildung 3: Abfrage der Data Dictionary View USER_OBJECTS mit SQL Injection

Dies ist natürlich nicht nur für SQL SELECT-Kommandos bedeutsam - auch INSERT, UPDATE oder DELETE-Kommandos könnten so verändert werden.  Insofern bestünde nicht nur die Gefahr des "Ausspähens", sondern auch die der Manipulation. Wie jedoch bereits oben erwähnt: Mit Application Express lassen sich solche Probleme leicht beheben.

Eine Möglichkeit ist das Application Express-Feature: Session State Security. Mit Session State Security lässt sich das Ändern von Elementen durch Manipulationen an der URL verhindern. Manchmal ist es jedoch beabsichtigt, dass Parameter durch URL direkt gesetzt werden (URL-Versand per Mail). Auch ohne Session State Security gilt jedoch: Beachtet man als Application Express-Entwickler einige Grundsätze, so sind diese Angriffe leicht zu verhindern. 

1. Verwenden Sie BIND-Variablen!

In obigem Beispiel muss das Berichts-SQL nur marginal verändert werden - die Sicherheits-Wirkung ist um so größer ...

select * from EMP where deptno = :P1_DEPTNO

Ob Sie ein Element mit &NAME. oder mit :NAME ansprechen, macht für SQL und PL/SQL einen gewaltigen Unterschied. In beiden Fällen wird der Name des Elements durch dessen Inhalt ersetzt. Wenn Sie &NAME. verwenden, geschicht dies jedoch vor Ausführung der SQL-Abfrage. Wenn Sie :NAME verwenden, wird die SQL-Abfrage zuerst gelesen und analysiert - erst dann findet die Ersetzung statt. Die Folge: Im ersten Fall kann die Struktur des SQL-Kommandos noch (bspw. durch ein zusätzliches OR 1=1) verändert werden - im zweiten Fall nicht mehr.

Gehen wir das nochmal durch:

select * from EMP where deptno = '&P1_DEPTNO.'

Wenn Sie P1_DEPTNO nun auf "99' or 1=1 --" setzen, sieht das SQL danach so aus ...

select * from EMP where deptno = '99' or 1=1 --'

Das "--" kommentiert in einem SQL-Kommando den Rest der Zeile aus. So einfach war also eine SQL-Injection!

Dem gegenüber stehen die Bind-Variablen. In diesem Fall findet keine Ersetzung auf String-Ebene statt - Die Datenbank liest und analysiert die Abfrage vor der Ersetzung - erst danach wird :P1_DEPTNO ersetzt. Probieren Sie es aus: Ändern Sie den Bericht entsprechend um und testen Sie es nochmals ...

SQL Injection-Versuch mit Bind-Variablen
Abbildung 4: SQL Injection-Versuch mit Bind-Variablen

Im besten Fall sehen Sie einfach keine Zeilen oder Sie bekommen (wie in Abbildung 4) eine Fehlermeldung. Wenn Ihre Berichte, wie in diesem Beispiel dargestellt, eine feste Struktur haben, sollten Sie stets Bind-Variablen (:NAME) verwenden und die einfache Ersetzung &NAME. unterlassen.

Besonderes Augenmerk auf mögliche SQL-Injection sollten Sie legen, wenn Sie dynamisches SQL verwenden - wenn Sie also einen PL/SQL-Block, welcher ein SQL-Kommando zurückliefert, als Berichtsquelle verwenden. So ist die folgende Berichtsdefinition problematisch  ...

declare
v_sql varchar2(32767);
begin
v_sql := 'select ';
v_sql := v_sql || ' empno, ';
v_sql := v_sql || ' ename, ';
v_sql := v_sql || 'from emp ';
v_sql := v_sql || 'where deptno = '||:P1_DEPTNO;
return v_sql;
end;

Application Express führt in diesem Fall zunächst den PL/SQL-Block aus. Dieser gibt in der Variable v_sql eine SQL-Abfrage zurück, welche anschließend als Berichtsabfrage ausgeführt wird. Hier wurde offensichtlich eine Bind-Variable (:P1_DEPTNO) verwendet - das sieht auf den ersten Blick also gut aus ...

... ist es aber nicht. Analysieren Sie zunächst, welchen Inhalt die Variable v_sql nach Ausführung des PL/SQL-Blocks hat - genau dies wird anschließend als Berichtsabfrage verwendet (angenommen P1_DEPTNO sei 10).

select 
empno,
ename
from emp
where deptno = 10

Die Ersetzung wurde schon im vom PL/SQL-Block im ersten Schritt vorgenommen - daher kann die eigentliche SQL-Abfrage nicht mehr von der Bind-Variable profitieren (Im Grunde genommen ist es so als hätten Sie &P1_DEPTNO. verwendet). Besser wäre es, das SQL wie folgt zusammenzusetzen:

declare
v_sql varchar2(32767);
begin
v_sql := 'select ';
v_sql := v_sql || ' empno, ';
v_sql := v_sql || ' ename, ';
v_sql := v_sql || 'from emp ';
v_sql := v_sql || 'where deptno = :P1_DEPTNO';
return v_sql;
end;

In diesem Fall wird die Bind-Variable auch in der Variable v_sql zurückgegeben. 

Nun gibt es allerdings einige Fälle, in denen die Verwendung von Bind-Variablen nicht möglich ist. So dürfen Bind-Variablen nur für Abfragen (SELECT) und DML-Anweisungen (INSERT, UPDATE, DELETE) verwendet werden - und dort auch nicht überall, sondern nur als Parameter in der WHERE-Klausel oder in der SELECT-Liste. Bind-Variablen können niemals genutzt werden, um Tabellen- oder Spaltennamen variablel zu gestalten. Das führt zum zweiten Mittel gegen SQL Injection.

2. Parameter vor Verwendung im SQL überprüfen: DBMS_ASSERT

Wenn die Verwendung von Bindevariablen nicht möglich ist, müssen die Parameter vor dem "Einbauen" ins SQL überprüft werden. Dazu gibt es ein eigenes PL/SQL-Paket: DBMS_ASSERT (Dokumentation). Dieses Paket schauen wir uns nun anhand eines Beispiels an. Eine Berichtsabfrage soll dynamisch sein; die selektierte Spalte und die Tabellen ergeben sich aus APEX-Elementen.

declare
 v_sql varchar2(32767);
begin
 v_sql := 'select ';
 v_sql := v_sql || :P1_SELECTED_COLUMN ||' ';
 v_sql := v_sql || 'from '||:P1_TABLE ||' ';
 v_sql := v_sql || 'where deptno = :P1_DEPTNO';
 return v_sql;
end;

Weder die Spalte (P1_SELECTED_COLUMN) noch die Tabelle (P1_TABLE) können als Bindevariablen festgelegt werden. Die Datenbank muss Tabellennamen und selektierte Spalten schließlich kennen, wenn das SQL geparst wird. Das "Einbauen" der Elementinhalte in die SQL-Abfrage ist hier also unumgänglich. Also müssen die Inhalte vorher mit DBMS_ASSERT geprüft werden. Wenn es an Datenbank-Objekte (Tabellen, Spalten, Views, Prozeduren und andere) geht, ist die Funktion ENQUOTE_NAME die Richtige. In SQL*Plus sieht die Verwendung wie folgt aus: Ein übergebener Objektname wird von der Funktion in doppelte Anführungszeichen eingeschlossen und zurückgegeben.

SQL> select dbms_assert.enquote_name('EMP') from dual;

DBMS_ASSERT.ENQUOTE_NAME('EMP')
--------------------------------------------------------------------

"EMP"

Diese Zeichenfolge kann nun gefahrlos als Tabellen-, Spalten oder sonstiger Objektname in die SQL-Abfrage eingebaut werden. Durch die doppelten Anführungszeichen ist deutlich, dass der komplette Inhalt als (Objekt-)Name aufgefasst werden soll.

Wie reagiert die Funktion jedoch auf einen SQL-Injection-Angriff? Ein Beispiel: Der Angreifer setzt den Text EMP" -- in das APEX-Element ein (Das Ziel ist, den Objektnamen selbst mit einem doppelten Anführungszeichen abzuschließen und den Rest auszukommentieren).

SQL> select dbms_assert.enquote_name('EMP" --') from dual;
select dbms_assert.enquote_name('EMP" --') from dual
       *
FEHLER in Zeile 1:
ORA-06502: PL/SQL: numerischer oder Wertefehler
ORA-06512: in "SYS.DBMS_ASSERT", Zeile 310
ORA-06512: in "SYS.DBMS_ASSERT", Zeile 354
ORA-06512: in Zeile 1

DBMS_ASSERT liefert den Text nicht mehr zurück - es wird dagegen eine Fehlermeldung ausgelöst. Damit ist deutlich, dass hier eine SQL-Injection versucht wurde - der Entwickler muss darauf reagieren - so wie es sein soll.

Die zweite wichtige Funktion in DBMS_ASSERT ist ENQUOTE_LITERAL. Sie funktioniert wie folgt:

SQL> select dbms_assert.enquote_literal(q'#Application Express#') text from dual;

TEXT
--------------------------------------------------------------------------------

'Application Express'

Diese Funktion schließt den übergebenen Text in einfache Anführungszeichen ein - damit kann er als Literal in einer SQL-Abfrage verwendet werden. Diesen Test sollten Sie für Parameter in der SELECT-Liste oder WHERE-Klausel dann durchführen, wenn Sie (aus welchen Gründen auch immer) keine Bindevariablen verwenden möchten. Versuchen Sie nochmals einen "Angriff":

SQL> select dbms_assert.enquote_literal(q'#Application ' or 1=1 --#') from dual;

select dbms_assert.enquote_literal(q'#Application ' or 1=1 --#') from dual
       *
FEHLER in Zeile 1:
ORA-06502: PL/SQL: numerischer oder Wertefehler
ORA-06512: in "SYS.DBMS_ASSERT", Zeile 310
ORA-06512: in "SYS.DBMS_ASSERT", Zeile 369

Wiederum hat DBMS_ASSERT den Angriff erkannt; sobald ein einfaches Hochkomma im Text vorkommt, wird eine SQL Injection-Attacke vermutet. Doch es gibt ab und zu immer noch Fälle, in denen das einfache Hochkomma nötig und wichtig ist. Wenn man bspw. den Namen O'Neill übergeben möchte, sähe das so aus.

SQL> select dbms_assert.enquote_literal(q'#O''Neill#') from dual;

DBMS_ASSERT.ENQUOTE_LITERAL(Q'#O''NEILL#')
-------------------------------------------------------------------------

'O''Neill'

Wird ein Hochkomma also in Oracle-typischer Manier maskiert (verdoppelt), so wird es von DBMS_ASSERT akzeptiert - schließlich kann es dann auch keine SQL Injection mehr auslösen.

Als Zusammenfassung wenden wir nun DBMS_ASSERT auf obiges Beispiel für einen Bericht mit dynamischer SQL-Abfrage an:

declare
 v_sql varchar2(32767);
begin
 v_sql :=          'select ';
 v_sql := v_sql || dbms_assert.enquote_name(:P1_SELECTED_COLUMN) ||' ';
 v_sql := v_sql || 'from ' || dbms_assert.enquote_name(:P1_TABLE) ||' ';
 v_sql := v_sql || 'where deptno = ''' || dbms_assert.enquote_literal(:P1_DEPTNO) || '''';
 return v_sql;
end;

Nimmt man als EMP für P1_TABLE, SAL für P1_SELECTED_COLUMN und 30 für P1_DEPTNO, so würde folgende SQL-Abfrage generiert und ausgeführt.

 select "SAL"
 from "EMP"
 where deptno = '30'

Zusammenfassung

Mit den hier vorgestellten Regeln sind Application Express-Anwendungen sicher vor SQL Injection-Angriffen.

  1. Nutzen Sie stets Bindevariablen (Doppelpunkt-Syntax) in SQL-Abfragen und niemals die Ersetzungszeichenfolgen (&-Syntax).
  2. Wenn Bindevariablen nicht genutzt werden, prüfen Sie alle Parameter vor dem Einbauen in das SQL-Kommando mit DBMS_ASSERT.

Zurück zur Community-Seite