22. September 2008

Schutz vor SQL Injection with DBMS_ASSERT

English title: SQL Injection protection with DBMS_ASSERT

Vor ein paar Wochen hat der Patrick Wolf mich auf den fehlenden SQL Injection-Schutz in einem Blog-Posting hingewiesen. Nun, SQL Injection wird ja ziemlich stark diskutiert ... aber der von der Datenbank angebotene Schutzmechanismus DBMS_ASSERT ist kaum bekannt - also habe ich mich entschlossen, dem ein (oder vielleicht auch mehrere) Postings zu widmen.
Some weeks ago Patrick Wolf recognized my missing SQL injection protection in a previous blog posting. Now ... the topic "as-is" is discussed more and more ... but the protection package which the database offers (DBMS_ASSERT) is hardly known by anyone ... So I dediced to write (at least) one posting about it.
SQL Injection ist ja bekanntlich (wie der Name schon sagt) das "Injizieren" von SQL-Code in die Datenbank durch eine entsprechende Schwachstelle in der Applikation. Solche Angriffe finden über das ganz normale Frontend der Applikation statt - nur dass der Angreifer dort Werte eingibt, an die ein "normaler" Nutzer nicht im Traum denkt. SQL Injection-Lücken entstehen dann, wenn Entwickler die Benutzereingaben ohne weitere Prüfung (per Stringverkettung) in SQL-Kommandos einfügen. So ist der folgende Java bzw. JDBC-Code denn auch ein typisches Beispiel für eine SQL Injection-Lücke.
As most know SQL injection is (as the name already tells) the "injection" of SQL commands into the database using the vulnerability in the application. SQL injection is done via the application's front end every other user uses. But an attacker enters values which most normal users don't even think about. SQL injection vulnerabilities arise when the application developer concatenates user-supplied parameters without checking into SQL queries and statements. So the following Java (JDBC) code is a typical example for a SQL injection vulnerability.
 :
 int iEmpno = request.getParameter("EMPNO");
 :
 PreparedStatement pstmt = con.prepareStatement("select sal from emp where empno = " + iEmpno);
 ResultSet rs = pstmt.executeQuery();
 while (rs.next()) {
   // Resultset hier auslesen
 }
 rs.close();
 pstmt.close();
 :
Wenn ein Angreifer nun als HTTP-Parameter EMPNO anstelle von bspw. 7839 ein 7839 or 1=1 übergibt, werden alle Datensätze selektiert. Neben dem einfachen or 1=1 könnte auch ein UNION ALL (...) mit einer Dictionary Tabelle eingebaut werden - so bekäme der Angreifer eine Übersicht über die vorhandenen PL/SQL-Prozeduren und Funtionen - alle SQL-Funktionen, für die das Datenbankschema Privilegien hat, lassen sich dann ebenfalls bequem durch die SQL-Abfrage ausführen.
Generall kann man sagen, dass in den meisten Fällen, in denen eine Sicherheitslücke in der Datenbank ausgenutzt wird, der Angriff zunächst über eine SQL Injection-Lücke erfolgt. SQL Injection ist quasi die "Eintrittskarte in die Datenbank". Je nachdem, wer dann da eingetreten ist und welche Absichten er hat, wird mehr oder weniger Schaden angerichtet.
Now let's assume that the end user provides not the expected value (e.g. 7839) but 7839 or 1=1 as the HTTP parameter EMPNO. Now the whole table gets selected. But SQL injection allows not only to bypass a query filter. Providing 7839 union all (...) allows to select other tables (including dictionary tables) and (of course) every accessible SQL function can now be executed.
Generally most attacks to Oracle databases firstly use a SQL injection vulnerability to get into the database. SQL injection is (so-to-say) the attackers' entry ticket to the database. Depending on who had entered it and what his or her objectives are, more or less damage is produced.
Zum Glück ist es recht einfach, SQL Injection-Lücken zu schließen. Aber der größte Feind des Entwicklers ist, dass der Schutz vor SQL Injection konsequent und umfassend sein muss - man muss eben immer dran denken. Die Lücke in obigem Beispiel ließe sich am einfachsten durch die Verwendung von Bindevariablen schließen. Die Verwendung von Bindevariablen bringt nicht nur bessere Performance, sondern i.d.R. auch Schutz vor SQL Injection - einfach weil das SQL-Kommando dann nicht mehr dynamisch ist.
In PL/SQL sind die Dinge einfach ... denn wenn man folgende Prozedur hat ...
Fortunately it's quite easy to prevent SQL injection attacks - but the fact that SQL injection protection must be comprehensive over each and every dynamic SQL statement is the developer's greatest enemy. You just have always to think about it. The vulnerability in the above java code is easy to handle: Just use bind variables. Using bind variables not only gives you better performance - it also prevents SQL injection since the SQL query is no longer a "dynamic one".
In PL/SQL things are easy ... if you have the following procedure ...
create or replace procedure doit(p_empno in number)
is
  v_sal EMP.SAL%TYPE;
begin
  select sal into v_sal from emp where empno = p_empno;
end;
... dann macht die Datenbank daraus ohnehin eine Bindevariable - tatsächlich wird als SQL dann ausgeführt:
... the database uses bind variables anyway - in fact it executed the following SQL query:
select sal into :1 from emp where empno = :2
Also ist SQL Injection kein Thema für PL/SQL-Programmierer? Mitnichten - auch in PL/SQL werden häufig dynamische SQL-Statements zusammengesetzt und gerade dann kann können SQL Injection-Lücken auch im PL/SQL-Code sein. Sobald Ihr mit dem Paket DBMS_SQL oder den Sprachkonstrukten EXECUTE IMMEDIATE bzw. OPEN ... FOR arbeitet, ist SQL Injection ein Thema.
So SQL Injection is a no-brainer for PL/SQL developers? No - PL/SQL allows also to construct dynamic SQL statements - and in this cases SQL injection vulnerabilities are possible. If you're using the DBMS_SQL package or the EXECUTE IMMEDIATE or OPEN ... FOR commands you have to think about SQL injection.
create or replace procedure doit(
  p_table    in varchar2, 
  p_valuecol in varchar2,
  p_wherecol in varchar2,
  p_param    in varchar2
)
is
  v_value varchar2(32767);
  v_sql   varchar2(32767);
begin
  v_sql := 'select '||p_valuecol||' from '||p_table||' where '||p_wherecol||' = '''||p_param||'''';
  execute immediate v_sql into v_value;
end;
Dieses Beispiel ist nun für SQL Injection-Angriffe anfällig; denn als Parameter können beliebige Texte übergeben werden - es können wieder alle erreichbaren SQL- und PL/SQL-Funktionen aufgerufen und alle erreichbaren Tabellen und Views selektiert werden.
This example is a PL/SQL which could be used for SQL injection attacks, since you can provide any arbitrary string for the procedure parameters. Since the parameters are concatenated into the SQL command without any checking an attacker could again: select any accessible table and execute any accessible function.
Doch nun zur Lösung. Mittlerweile gibt es das PL/SQL-Paket DBMS_ASSERT, mit dem der PL/SQL-Programmierer seine zusammengesetzten SQL-Anweisungen prüfen kann. Die wichtigsten Funktionen sind ENQUOTE_NAME und ENQUOTE_LITERAL. Beide Funktionen arbeiten so, dass der übergebene Wert in Quotes eingeschlossen und zurückgegeben wird, wenn alles in Ordnung ist und dass ein Fehler ausgelöst wird, wenn ein SQL Injection-Angriff erkannt wird.
Now we come to the solution: The package DBMS_ASSERT can be used to check the user-supplied parameters before concatenating them into the SQL statement. The most important functions are ENQUOTE_NAME and ENQUOTE_LITERAL. Both functions enquote the supplied parameter if the parameter is OK and raise an exception if there's a SQL injection attack.
SQL> select dbms_assert.enquote_literal('KING') from dual;

DBMS_ASSERT.ENQUOTE_LITERAL('KING')
--------------------------------------------------------------------------------
'KING'

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

DBMS_ASSERT.ENQUOTE_NAME('EMP')
--------------------------------------------------------------------------------
"EMP"
So wie der Wert zurückkommt kann er nun ins SQL eingebaut werden. Im folgenden nun ein paar Beispiele, wie DBMS_ASSERT reagiert, wenn SQL Injection-Angriffe versucht werden
.
As this value returns it may be built into the SQL statement. The following are examples for DBMS_ASSERT behaviour when SQL injection is detected.
  • Der "klassische" Angriff: Übergabe von KING' or 1=1 -- (Quote schließen, eigene Bedingung anhängen und Rest auskommentieren)
    SQL> select dbms_assert.enquote_literal('KING''or 1=1') from dual;
    select dbms_assert.enquote_literal('KING''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
    
    ... Wenn DBMS_ASSERT also einen Fehler auslöst, besteht SQL Injection-Gefahr.

  • Die Übergabe von O'Neill muss jedoch gehen (damit wir mit den Quotes nicht durcheinanderkommen, verwenden wir die ab 10gR2 verfügbare neue Quote-Syntax).
    SQL> select dbms_assert.enquote_literal(q'#O''Neill#') from dual;
    
    DBMS_ASSERT.ENQUOTE_LITERAL(Q'#O''NEILL#')
    --------------------------------------------------------------------------------
    'O''Neill'
    
  • Objektnamen müssen mit ENQUOTE_NAME behandelt werden. Grund sind die unterschiedlichen "Quote"-Zeichen - Objektnamen müssen mit doppelten Anführungszeichen versehen werden.
    SQL> select dbms_assert.enquote_name('EMP" union all (select 1 from dual)--') from dual;
    select dbms_assert.enquote_name('EMP" union all (select 1 from dual)--') 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
    
  • Ein Versuch mit ENQUOTE_LITERAL erkennt die SQL Injection nicht. Man sieht, wie wichtig es ist, die richtige Funktion im Paket DBMS_ASSERT zu benutzen.
    SQL> select dbms_assert.enquote_literal('EMP" union all (select 1 from dual)--') from dual;
    
    DBMS_ASSERT.ENQUOTE_LITERAL('EMP"UNIONALL(SELECT1FROMDUAL)--')
    --------------------------------------------------------------------------------
    'EMP" union all (select 1 from dual)--'
    
    
  • The "classic" attack: Passing KING' or 1=1 -- (close the quote, add own condition and comment the rest out)
    SQL> select dbms_assert.enquote_literal('KING''or 1=1') from dual;
    select dbms_assert.enquote_literal('KING''or 1=1') from dual
           *
    FEHLER in Zeile 1:
    ORA-06502: PL/SQL: numeric or value error
    ORA-06512: in "SYS.DBMS_ASSERT", Zeile 310
    ORA-06512: in "SYS.DBMS_ASSERT", Zeile 369
    
    ... so we see: if DBMS_ASSERT raises an exception, SQL injection was detected.
  • Let's check another literal: O'Neill has an apostroph - does this also lead to a SQL injection alert? For better readability we use the new quote syntax intruduced in 10gR2.
    SQL> select dbms_assert.enquote_literal(q'#O''Neill#') from dual;
    
    DBMS_ASSERT.ENQUOTE_LITERAL(Q'#O''NEILL#')
    --------------------------------------------------------------------------------
    'O''Neill'
    
  • Object (table, view, package and other) names have to be treated with ENQUOTE_NAME. Reason is the different quote character (object names are quoted with the double quote)
    SQL> select dbms_assert.enquote_name('EMP" union all (select 1 from dual)--') from dual;
    select dbms_assert.enquote_name('EMP" union all (select 1 from dual)--') from dual
           *
    ERROR in line 1:
    ORA-06502: PL/SQL: numeric or value error
    ORA-06512: in "SYS.DBMS_ASSERT", Zeile 310
    ORA-06512: in "SYS.DBMS_ASSERT", Zeile 354
    ORA-06512: in line 1
    
  • An attempt using ENQUOTE_LITERAL does not detect the SQL Injection attack - it's obvious that using the correct function within DBMS_ASSERT is essential.
    SQL> select dbms_assert.enquote_literal('EMP" union all (select 1 from dual)--') from dual;
    
    DBMS_ASSERT.ENQUOTE_LITERAL('EMP"UNIONALL(SELECT1FROMDUAL)--')
    --------------------------------------------------------------------------------
    'EMP" union all (select 1 from dual)--'
    
Extrem wichtig ist also die richtige Verwendung von ENQUOTE_NAME bzw. ENQUOTE_LITERAL. Wenn man versucht, SQL Injection-Angriffe auf Objektnamen mit ENQUOTE_LITERAL zu erkennen, geht das genauso schief wie der Versuch auf Literalen mit ENQUOTE_NAME. Verwendet immer die richtige DBMS_ASSERT-Funktion, je nachdem, ob Ihr ein Literal oder einen Datenbank-Objektnamen in euer SQL einbaut. Obige PL/SQL-Prozedur würde dann so aussehen:
It is absolutely essential to use ENQUOTE_NAME or ENQUOTE_LITERAL in the correct context. Attempts to detect SQL injection attacks on object names with ENQUOTE_LITERAL fails as well as with ENQUOTE_NAME on literals. Using the correct function in the correct context makes cure that SQL injection attacks will be detected. Applied to the above PL/SQL procedure the code looks as follows:
create or replace procedure doit(
  p_table    in varchar2, 
  p_valuecol in varchar2,
  p_wherecol in varchar2,
  p_param    in varchar2
)
is
  v_value varchar2(32767);
  v_sql   varchar2(32767);
begin
  v_sql := 'select '||dbms_assert.enquote_name(p_valuecol)||' '||
           'from '||dbms_assert.enquote_name(p_table)||' '||
           'where '||dbms_assert.enquote_name(p_wherecol)||' = '||dbms_assert.enquote_literal(p_param);
  execute immediate v_sql into v_value;
end;
/
Wenn man es richtig einsetzt, kann man seine PL/SQL-Prozeduren mit DBMS_ASSERT also recht gut gegen SQL Injection-Angriffe absichern. Ich würde sogar so weit gehen: Die Verwendung von DBMS_ASSERT ist bei dynamischem SQL inzwischen zur absoluten Pflicht geworden (ich muss mich bei meinen künftigen Postings nur selbst dran halten).
Übrigens: DBMS_ASSERT ist zwar erst ab Version 11 dokumentiert, wurde jedoch auf 9.2.0.8 und 10.2.0.3 portiert; ist also in allen aktuellen Datenbankversionen vorhanden.
DBMS_ASSERT is a nice approch to fix SQL injection vulnerabilities in PL/SQL code. The important thing is to use it correctly (say: the correct function). I'd so far to say that the usage of DBMS_ASSERT is a must when using dynamic SQL statements in PL/SQL code. The only thing I have to do in the future is to keep on this.

Kommentare:

Anonym hat gesagt…

wenn ich dies richtig interpretiere,
dann habe ich mich mit der google-nutzung einverstanden erklärt,
ohne dies vorher zu wissen und zu lesen und mich entscheiden zu können, da ich ja erst den text lesen kann, NACHDEM ich auf der Site bin, womit ich mich dann aber bereits einverstanden erklärt habe, Hhmmm.... dreht sich schöön im Kreis :-)
Wozu haben Sie sowas auf Ihrer WebSite ?


Zitat :

StatCounter - Free Web Tracker and Counter
Seitenzugriffe
View my Stats

Daneben benutzt diese Webseite Google Analytics, einen Webanalysedienst der Google Inc. („Google“) Google Analytics verwendet sog. „Cookies“, Textdateien, die auf Ihrem Computer gespeichert werden und die eine Analyse der Benutzung der Website durch Sie ermöglicht. Die durch den Cookie erzeugten Informationen über Ihre Benutzung diese Website (einschließlich Ihrer IP-Adresse) wird an einen Server von Google in den USA übertragen und dort gespeichert. Google wird diese Informationen benutzen, um Ihre Nutzung der Website auszuwerten, um Reports über die Websiteaktivitäten für die Websitebetreiber zusammenzustellen und um weitere mit der Websitenutzung und der Internetnutzung verbundene Dienstleistungen zu erbringen. Auch wird Google diese Informationen gegebenenfalls an Dritte übertragen, sofern dies gesetzlich vorgeschrieben oder soweit Dritte diese Daten im Auftrag von Google verarbeiten. Google wird in keinem Fall Ihre IP-Adresse mit anderen Daten der Google in Verbindung bringen.

Sie können die Installation der Cookies durch eine entsprechende Einstellung Ihrer Browser Software verhindern.
>>
Durch die Nutzung dieser Website erklären Sie sich mit der Bearbeitung der über Sie erhobenen Daten durch Google in der zuvor beschriebenen Art und Weise und zu dem zuvor benannten Zweck einverstanden.“
>>

Carsten Czarski hat gesagt…

Warum mache ich das?

Ich möchte sehen, welche Teile des Blogs gelesen werden und welche nicht - schlicht und einfach möchte ich wissen, ob das überhaupt jemanden interessiert, was ich hier schreibe ... wenn ich feststelle, dass es niemand liest, werde ich es lassen ...

Warum der Hinweis?

Um offen zu sein. Nachdem Sie das gelesen haben, können Sie in der Zukunft agieren: Entweder die Seite nicht mehr besuchen, JavaScript für Google Analytics verbieten (bspw. mit "NoScript") oder die Cookies für Google verbieten (per Browser-Einstellung). Wenn ich nichts schreiben würde, wäre das nicht so offen ...

Und zu guter Letzt:

Es ist Teil der Bedingungen von Google Analytics, einen Hinweis dieser Art aufzunehmen ...

Viele Grüße

-Carsten

Beliebte Postings