SQL Injection erschweren durch DBMS_ASSERT

05.
Dezember
2015
Veröffentlicht von: Hildegard Asenbauer

In PL/SQL empfiehlt es sich aus vielen Gründen, mit statischem SQL zu arbeiten, soweit das möglich ist; genannt seien hier vor allem Wartbarkeit und Performance-Vorteile.

In PL/SQL empfiehlt es sich aus vielen Gründen, mit statischem SQL zu arbeiten, soweit das möglich ist; genannt seien hier vor allem Wartbarkeit und Performance-Vorteile. Und so ganz nebenbei verwendet man automatisch Bind-Variablen, ohne groß etwas dafür tun zu müssen: Übergabe-Parameter von Prozeduren und Funktionen werden intern als Bind-Variablen umgesetzt, wenn sie in einem SQL-Befehl verwendet werden.

Nun gibt es aber Fälle, in denen man nicht um dynamisches SQL herumkommt. Solange dabei von außen ausschließlich Werte übergeben werden, aber keine Spalten- oder Tabellennamen, kann man sich teilweise auch hier mit einer Syntax behelfen, die Bind-Variablen benutzt: USING. Ein einfaches Beispiel dazu:  

CREATE OR REPLACE PROCEDURE update_sal( p_ename IN emp.ename%TYPE, p_sal in emp.sal%type)
IS
BEGIN
   EXECUTE IMMEDIATE
   'UPDATE EMP
       SET sal = :a
     WHERE ename = :b' using p_sal, p_ename;

END update_sal;

Oder allgemein:
EXECUTE IMMEDIATE '... WHERE spalte = :a' USING p_val;

Bei Ref Cursorn:
OPEN ref_cur FOR 'SELECT .... WHERE spalte = :a' USING p_val;

Das ist aber nicht immer praktikabel. Und spätestens dann, wenn Objektnamen oder Spaltennamen übergeben werden, ist es vorbei mit der Verwendung von Bind-Variablen. Hier muss man mit Konkatenierung arbeiten.

Eine Möglichkeit, sich in solchen Fällen vor SQL Injection zu schützen, bietet das Package DBMS_ASSERT. Es beinhaltet eine Reihe von Funktionen, die übergebene Werte nach unterschiedlichen Kriterien überprüfen oder ändern.

 

Gehen wir einmal von folgender Funktion aus, die nur das Prinzip verdeutlichen soll (ausführlichere Beispiele zu SQL Injection gibt es genügend im Netz):

CREATE TYPE vt AS TABLE OF VARCHAR2 (100);
/

CREATE OR REPLACE FUNCTION get_werte (p_such_spalte    IN VARCHAR2,
                                      p_tabelle        IN VARCHAR2,
                                      p_where_spalte   IN VARCHAR2,
                                      p_wert           IN VARCHAR2)
   RETURN vt
IS
   v_cur   SYS_REFCURSOR;
   v_sql   VARCHAR2 (2000);
   v_arr   vt;
BEGIN
   v_sql  := 'select ' || p_such_spalte || ' from ' || p_tabelle
              || ' where ' || p_where_spalte || ' = ''' || p_wert||'''';

   OPEN v_cur FOR v_sql;

   FETCH v_cur BULK COLLECT INTO v_arr;

   CLOSE v_cur;

   RETURN v_arr;
END get_werte;
/

SELECT COLUMN_VALUE 
FROM TABLE (get_werte ('sal','scott.emp', 'ename', 'SMITH'))
/
COLUMN_VALUE                                                                   
--------------------------------------------------------------------------------
800     

SELECT COLUMN_VALUE 
FROM TABLE (get_werte ('sal','scott.emp', 'ename', 'SMITH''  OR 1=1 --'))
/
COLUMN_VALUE                                                                   
--------------------------------------------------------------------------------
800                                                                            
...                                                                           
1300                                                                           

14 rows selected.

Es leuchtet ein, dass p_wert in anderer Form abgesichert werden muss als die übrigen drei Parameter.  Mit DBMS_ASSERT können beide Arten von Eingaben abgesichert werden:

  • Eingegebene Werte (wie p_wert) können mit ENQUOTE_LITERAL abgesichert werden
  • Eingegenebe Bezeichner (Spalten- und Tabellennamen wie die übrigen drei Parameter) können mit ENQUOTE_NAME abgesichert werden

Ausserdem können eingegenebe Tabellennamen mit SIMPLE_SQL_NAME, QUALIFIED_SQL_NAME, SQL_OBJECT_NAME und SCHEMA_NAME überprüft werden. Bei diesen Funktionen wird der übergebene Wert nicht verändert, aber wenn die Überprüfung fehlschlägt, führt dies zu einem Fehler.

Diese Funktionen wollen wir uns nun näher anschauen.

ENQUOTE_LITERAL

Diese Funktion dient zur Absicherung eingegbener Literale, also String-Werte. Sie umgibt den übergebenen Wert mit einfachen Hochkommata (die man dann natürlich im Quelltext nicht mehr mühsam ergänzen muss) und überprüft, ob er intern ein einzelnes einfaches Hochkomma enthält. Letzeres führt zu einem Fehler. Gepaarte einfache Hochkommata sind erlaubt, weil ja auch Werte vorkommen können, die ein solches enthalten (Stichwort: Escape).

Damit soll verhindert werden, dass durch die Hintertür eine Zusatzbedingung á la "SMITH' OR 1=1 --" mit angegeben wird.

SELECT DBMS_ASSERT.ENQUOTE_LITERAL ( 'SMITH') FROM DUAL;
DBMS_ASSERT.ENQUOTE_LITERAL('SMITH')                                           
--------------------------------------------------------------------------------
'SMITH' 

SELECT DBMS_ASSERT.ENQUOTE_LITERAL ( 'O''''Conner') FROM DUAL;
DBMS_ASSERT.ENQUOTE_LITERAL('O''''CONNER')                                     
--------------------------------------------------------------------------------
'O''Conner' 

SELECT DBMS_ASSERT.ENQUOTE_LITERAL ( 'SMITH'' OR 1=1 --') FROM DUAL; -- Der klassische Fall
      
Error at line 1
ORA-06502: PL/SQL: numerischer oder Wertefehler
ORA-06512: in "SYS.DBMS_ASSERT", Zeile 409
ORA-06512: in "SYS.DBMS_ASSERT", Zeile 493

ENQUOTE_NAME

In analoger Weise arbeitet ENQUOTE_NAME, nur dass hier keine einfachen Hochkommata ergänzt werden, sondern doppelte. Standardmäßig wird der Bezeichner ausserdem in Großbuchstaben gewandelt, was aber durch einen zweiten Parameter unterbunden werden kann.

Wird ein Wert mitgegeben, der bereits von doppelten Hochkommata eingeschlossen ist, wird kein zweites Paar ergänzt, und die Schreibweise bleibt auch unverändert. Intern darf der Wert kein doppeltes Hochkomma enthalten; in Version 11g konnte man noch Paare von doppelten Hochkommata mitgeben, in 12c wird auch das unterbunden.

ENQUOTE_NAME dient rein der Absicherung der Eingabe: Alles, was sich innerhalb doppelter Hochkommata befinden, wird von Oracle als ein Bezeichner betrachtet - womit auch gesagt ist, dass die Methode nicht tauglich ist, wenn der Schemaname dem Objektnamen vorangestellt mitgegeben werden kann. Es findet keine inhaltliche Überprüfung statt.

SELECT DBMS_ASSERT.ENQUOTE_NAME('emp') FROM DUAL;
DBMS_ASSERT.ENQUOTE_NAME('EMP')                                                
--------------------------------------------------------------------------------
"EMP"   

SELECT DBMS_ASSERT.ENQUOTE_NAME('"emp"') FROM DUAL;
DBMS_ASSERT.ENQUOTE_NAME('"EMP"')                                              
--------------------------------------------------------------------------------
"emp"

SELECT DBMS_ASSERT.ENQUOTE_NAME('scott.emp') FROM DUAL;
DBMS_ASSERT.ENQUOTE_NAME('SCOTT.EMP')                                          
--------------------------------------------------------------------------------
"SCOTT.EMP"

Mit diesen beiden Funktionen kann obiges Beispiel nun umgebaut werden:

CREATE OR REPLACE FUNCTION get_werte (p_such_spalte    IN VARCHAR2,
                                      p_tabelle        IN VARCHAR2,
                                      p_where_spalte   IN VARCHAR2,
                                      p_wert           IN VARCHAR2)
   RETURN vt
IS
   v_cur   SYS_REFCURSOR;
   v_sql   VARCHAR2 (2000);
   v_arr   vt;
BEGIN
   v_sql  := 'select ' || DBMS_ASSERT.ENQUOTE_NAME(p_such_spalte)
             || ' from ' ||DBMS_ASSERT.ENQUOTE_NAME( p_tabelle)
             || ' where ' || DBMS_ASSERT.ENQUOTE_NAME(p_where_spalte)
             || ' = ' ||DBMS_ASSERT.ENQUOTE_LITERAL (p_wert);

   OPEN v_cur FOR v_sql;

   FETCH v_cur BULK COLLECT INTO v_arr;

   CLOSE v_cur;

   RETURN v_arr;
END get_werte;
/

SELECT COLUMN_VALUE  FROM TABLE (get_werte ('sal','emp', 'ename', 'SMITH'))
/
COLUMN_VALUE                                                                   
--------------------------------------------------------------------------------
800  

SELECT COLUMN_VALUE
FROM TABLE (get_werte ('sal','emp', 'ename', 'SMITH''  OR 1=1 --'))
/
Error at line 2
ORA-06502: PL/SQL: numerischer oder Wertefehler
ORA-06512: in "SYS.DBMS_ASSERT", Zeile 409
ORA-06512: in "SYS.DBMS_ASSERT", Zeile 493
ORA-06512: in "SCOTT.GET_WERTE", Zeile 11

SELECT COLUMN_VALUE  FROM TABLE (get_werte ('sal','scott.emp', 'ename', 'SMITH'))
/
Error at line 1
ORA-00942: Tabelle oder View nicht vorhanden

SIMPLE_SQL_NAME und QUALIFIED_SQL_NAME

Diese Funktionen überprüfen ausschießlich, ob der eingegebene Wert den Oracle-Namenskonventionen entspricht. Nicht überprüft wird dabei allerdings die Länge der Eingabe.

Der Unterschied zwischen den beiden Funktionen besteht darin, dass SIMPLE_SQL_NAME einen einzelnen Bezeichner erwartet, während QUALIFIED_SQL_NAME mit einem qualifizierten Oracle-Bezeichner umgehen kann. Letzerer kann Punkte und "@" enthalten.

Da innerhab von doppelten Hochkommata alles erlaubt ist, kann man damit diese Art der Überprüfung damit theoretisch aushebeln; allerdings taugt dann die Eingabe auch nicht mehr als Objektname.

SELECT DBMS_ASSERT.SIMPLE_SQL_NAME ( 'emp') FROM DUAL;
DBMS_ASSERT.SIMPLE_SQL_NAME('EMP')                                             
--------------------------------------------------------------------------------
emp

SELECT DBMS_ASSERT.SIMPLE_SQL_NAME('emp or 1=1') FROM DUAL;
Error at line 1
ORA-44003: Ungültiger SQL-Name
ORA-06512: in "SYS.DBMS_ASSERT", Zeile 206

SELECT DBMS_ASSERT.SIMPLE_SQL_NAME ( '"emp or 1=1"') FROM DUAL;
DBMS_ASSERT.SIMPLE_SQL_NAME('"EMPOR1=1"')                                      
--------------------------------------------------------------------------------
"emp or 1=1"  

SELECT DBMS_ASSERT.QUALIFIED_SQL_NAME ( 'scott.emp@test') FROM DUAL;
DBMS_ASSERT.QUALIFIED_SQL_NAME('SCOTT.EMP@TEST')                               
--------------------------------------------------------------------------------
scott.emp@test

SQL_OBJECT_NAME und SCHEMA_NAME

Hier findet auch eine inhaltliche Überprüfung statt. Das heisst, es müssen nicht nur die Namenskonventionen eingehalten werden, sondern das Objekt bzw. das Schema muss auch vorhanden sein (Ausnahme: Angabe eines Datenbank-Links).

Bei SQL_OBJECT_NAME kann ein Schemaname davor angegeben werden, muss aber nicht. Falls keiner angegeben wird, muss sich das Objekt im aktuellen Schema befinden. Hat ein User keine Zugriffsrechte auf das Objekt, schlägt die Überprüfung mit SQL_OBJECT_NAME ebenfalls fehl. 

SCHEMA_NAME arbeitet case-sensitive, SQL_OBJECT_NAME dagegen nicht.

-- als User SCOTT:

SELECT DBMS_ASSERT.SQL_OBJECT_NAME ( 'emp') FROM DUAL;
DBMS_ASSERT.SQL_OBJECT_NAME('EMP')                                              
--------------------------------------------------------------------------------
emp    

SELECT DBMS_ASSERT.SQL_OBJECT_NAME ( 'scott.emp') FROM DUAL:
DBMS_ASSERT.SQL_OBJECT_NAME('SCOTT.EMP')                                        
--------------------------------------------------------------------------------
scott.emp   

SELECT DBMS_ASSERT.SQL_OBJECT_NAME ( 'DUMMY.TESTTAB') FROM DUAL;

Error at line 1
ORA-44002: Ungültiger Objektname
ORA-06512: in "SYS.DBMS_ASSERT", Zeile 383

SELECT DBMS_ASSERT.SCHEMA_NAME ( 'scott') FROM DUAL;

Error at line 1
ORA-44001: Ungültiges Schema
ORA-06512: in "SYS.DBMS_ASSERT", Zeile 333

SELECT DBMS_ASSERT.SCHEMA_NAME ( 'SCOTT') FROM DUAL:
DBMS_ASSERT.SCHEMA_NAME('SCOTT')                                                
--------------------------------------------------------------------------------
SCOTT

Damit könenn wir unser obiges Beispiel nochmal umbauen, so dass es auch wieder mit vorangestellten Schemanamen umgehen kann:

CREATE OR REPLACE FUNCTION get_werte (p_such_spalte    IN VARCHAR2,
                                      p_tabelle        IN VARCHAR2,
                                      p_where_spalte   IN VARCHAR2,
                                      p_wert           IN VARCHAR2)
   RETURN vt
IS
   v_cur   SYS_REFCURSOR;
   v_sql   VARCHAR2 (2000);
   v_arr   vt;
BEGIN
   v_sql  := 'select ' || DBMS_ASSERT.ENQUOTE_NAME(p_such_spalte)
             || ' from ' ||DBMS_ASSERT.SQL_OBJECT_NAME( p_tabelle)
             || ' where ' || DBMS_ASSERT.ENQUOTE_NAME(p_where_spalte)
             || ' = ' ||DBMS_ASSERT.ENQUOTE_LITERAL (p_wert);

   OPEN v_cur FOR v_sql;

   FETCH v_cur BULK COLLECT INTO v_arr;

   CLOSE v_cur;

   RETURN v_arr;
END get_werte;
/

SELECT COLUMN_VALUE  FROM TABLE (get_werte ('sal','scott.emp', 'ename', 'SMITH'))
/
COLUMN_VALUE                                                                   
--------------------------------------------------------------------------------
800    

Fazit: Versierte Hacker werden wohl immer noch Mittel und Wege finden, aber schon allein der Einsatz von DBMS_ASSERT.ENQUOTE_LITERAL macht ihnen das Leben deutlich schwerer.

Weitere Informationen zu SQL Injection erhalten Sie in unserem Security und APEX Security Kurs.

DBMS_ASSERT SQL Injection

Jede Menge Know-how für Sie!

In unserer Know-How Datenbank finden Sie mehr als 300 ausführliche Beiträge zu den Oracle-Themen wie DBA, SQL, PL/SQL, APEX und vielem mehr.
Hier erhalten Sie Antworten auf Ihre Fragen.