SQL-Injection: wie wir uns absichern können.

Mischa Sameli, Geschäftsführer & Leiter Entwicklung

Wie bereits in einem früheren Post beschrieben, gibt es bei ColdFusion eine bestechend einfach Art, um SQL-Injections abzuwehren: den Tag cfqueryparam. Allerdings reicht dies nicht aus, um allen möglichen Bedrohungen etwas entgegen zu setzen.

Wie bereits in einem früheren Post beschrieben, gibt es bei ColdFusion eine bestechend einfach Art, um SQL-Injections abzuwehren: den Tag cfqueryparam. Allerdings reicht dies nicht aus, um allen möglichen Bedrohungen etwas entgegen zu setzen.
Dazu nochmals ein einfaches Beispiel:

1<cfquery name="GetUser" datasource="newsletter">
2 SELECT * FROM User
3 WHERE ID = #url.userID#
4</cfquery >
Hier wird der URL-Parameter userID direkt für die Datenbankabfrage verwendet. Ohne grossen Aufwand kann nun die komplette Tabelle gelöscht werden, indem nämlich dem URL-Parameter ein schädlicher SQL-Befehl mitgeliefert wird. Um dies zu unterbinden, benutzt man also in jedem Fall den cfqueryparam-Tag.
1<cfquery name="GetUser" datasource="newsletter">
2 SELECT * FROM User
3 WHERE ID = <cfqueryparam value="#url.userID#" cfsqltype="cf_sql_integer">
4</cfquery >
Ist nun der URL-Parameter nicht rein numerisch, wird die Datenbankabfrage nicht ausgeführt, sondern eine Fehlermeldung erzeugt. Gut gemacht, keine Gefahr. Soweit so gut, nun gibt es aber noch weitere Szenarien innerhalb einer Abfrage. Beispielsweise wenn man die Resultate auf deren 5 beschränken möchte. Auf Nummer sicher geht man, wenn man das cfquery-Attribut Maxrows verwendet:
1<cfquery name="GetUser" datasource="newsletter" maxrows="5">
2 SELECT * FROM User
3 WHERE ID = <cfqueryparam value="#url.userID#" cfsqltype="cf_sql_integer">
4</cfquery>
Wird der Wert ebenfalls über einen URL-Parameter dynamisiert. sähe dies wie folgt aus:
1<cfquery name="GetUser" datasource="newsletter" maxrows="#url.maxrows#">
2 SELECT * FROM User
3 WHERE ID = <cfqueryparam value="#url.userID#" cfsqltype="cf_sql_integer">
4</cfquery >
Soweit alles in Ordnung, keine Gefahr für uns. Aber, die Abfrage ist natürlich nicht optimiert. Etwas performanter sieht die gleiche Abfrage dann etwa so aus:
1MSSQL: <cfquery name="GetUser" datasource="newsletter">
2 SELECT TOP #url.maxrows# * FROM User
3 WHERE ID = <cfqueryparam value="#url.userID#" cfsqltype="cf_sql_integer">
4</cfquery>MySQL: <cfquery name="GetUser" datasource="newsletter">
5 SELECT * FROM User
6 WHERE ID = <cfqueryparam value="#url.userID#" cfsqltype="cf_sql_integer">
7LIMIT #url.maxrows#
8</cfquery >
So, und jetzt haben wir Schädlingen wieder Tür und Tor geöffnet. Und hier haben wir einen Fall, in welchem wir den cfqueryparam-Tag leider nicht verwenden können. Eine einfache Möglichkeit ist hier, nur den numerischen Wert zu mit der ColdFusion-Funktion Val() zu packen:
1MSSQL: <cfquery name="GetUser" datasource="newsletter">
2 SELECT TOP #Val(url.maxrows)# * FROM User
3 WHERE ID = <cfqueryparam value="#url.userID#" cfsqltype="cf_sql_integer">
4</cfquery>
So, in diesem Fall wird nur der numerische Teil des URL-Parameters verwendet. Alle anderen Zeichen werden nicht beachtet. Lässt sich überhaupt kein numerischer Wert finden, wird 0 verwendet, was dazu führt, dass die Abfrage keine Treffer zurückliefert – auch gut, oder? Wieder eine Hürde genommen, aber weitere stehen natürlich noch an. Was, wenn nämlich die zu verwendende Tabelle parametrisiert wird? Also beispielsweise url.table = newsletter? Aus die Maus und losgehts mit der Schweinerei! Auch oft gesehen und gerne verwendet: url.sort oder url.order um die Sortierspalte zu bestimmen oder auszutauschen. Auch hier kann der cfqueryparam nicht angewendet werden, und hier hilft auch sonst keine schlaue Funktion weiter. Hier sind wir gefragt, wir Programmierer. Und zwar bei der Überprüfung der Variablen, die wir an Datenbankabfragen weiterleiten. Ein simples Beispiel dazu:
1<!--- lokale Variablen deklarieren ---><cfparam name="orderby" default="stremail" />
2<!--- Variable vorhanden, orderby neu setzen --->
3<cfif StructKeyExists(url,"sort")>
4<cfswitch expression="#LCase(url.sort)#">
5<cfcase value="name">
6<cfset orderby = "strname">
7</cfcase><cfcase value="vorname">
8<cfset orderby = "strvorname">
9</cfcase><!--- Abfrage starten ---><cfquery name="GetUser" datasource="newsletter">
10 SELECT TOP #Val(url.maxrows)# * FROM User
11 WHERE ID = <cfqueryparam value="#url.userID#" cfsqltype="cf_sql_integer">
12ORDER BY #orderby#
13</cfquery>
So, Gefahr wiederum gebannt. Und um ganz auf Nummer sicher zu gehen, sollten alle anderen URL-Parameter, die direkt in der Datenbank-Anfrage benutzt werden, nach dem selben Schema validiert werden:
1<!--- lokale Variablen deklarieren ---><cfparam name="orderby" default="stremail" />
2<cfparam name="maxrows" default="" />
3<cfparam name="userID" default="0" />
4<!--- URL-Parameter auswerten und Variablen ggf neu setzen --->
5<cfif StructKeyExists(url,"sort")>
6<!--- Variable vorhanden, orderby neu setzen --->
7<cfswitch expression="#LCase(url.sort)#">
8<cfcase value="name">
9<cfset orderby = "strname">
10</cfcase><cfcase value="vorname">
11<cfset orderby = "strvorname">
12</cfcase><cfif StructKeyExists(url,"maxrows")>
13<cfset maxrows = " TOP " & Val(url.maxrows)>
14</cfif><cfif StructKeyExists(url,"user")>
15<cfset userID = Val(url.user)>
16</cfif><!--- Abfrage starten ---><cfquery name="GetUser" datasource="newsletter">
17 SELECT #maxrows# * FROM User
18 WHERE ID = <cfqueryparam value="#userID#" cfsqltype="cf_sql_integer">
19ORDER BY #orderby#
20</cfquery>
Ein so abgesicherte Abfrage ist kaum mehr auszutricksen – weniger Validierung ist schlicht fahrlässig.

PS: selbstverständlich sollte man lokale Variablen auch in den Variables-Scope schreiben – ich habs der Leserlichkeit wegen unterlassen.

PS 2: Es gibt zahlreiche Funktionen zur Filterung von schädlichem Code bei cflib.org. Wissen, was man tut ist aber immer besser als Gottvertrauen…