c# - tipos - Evitar la inyección de SQL sin parámetros
solucionar inyeccion sql (21)
Estamos teniendo otra discusión aquí en el trabajo sobre el uso de consultas SQL parametrizadas en nuestro código. Tenemos dos lados en la discusión: Yo y algunos otros que dicen que siempre debemos usar parámetros para protegernos contra las inyecciones de sql y los otros tipos que no creen que sea necesario. En su lugar, quieren reemplazar los apóstrofos individuales con dos apóstrofes en todas las cadenas para evitar las inyecciones de sql. Todas nuestras bases de datos ejecutan Sql Server 2005 o 2008 y nuestra base de código se ejecuta en .NET framework 2.0.
Déjame darte un ejemplo simple en C #:
Quiero que usemos esto:
string sql = "SELECT * FROM Users WHERE Name=@name";
SqlCommand getUser = new SqlCommand(sql, connection);
getUser.Parameters.AddWithValue("@name", userName);
//... blabla - do something here, this is safe
Mientras que los otros chicos quieren hacer esto:
string sql = "SELECT * FROM Users WHERE Name=" + SafeDBString(name);
SqlCommand getUser = new SqlCommand(sql, connection);
//... blabla - are we safe now?
Donde la función SafeDBString se define de la siguiente manera:
string SafeDBString(string inputValue)
{
return "''" + inputValue.Replace("''", "''''") + "''";
}
Ahora, siempre que utilicemos SafeDBString en todos los valores de cadena en nuestras consultas, deberíamos estar a salvo. ¿Derecha?
Hay dos razones para usar la función SafeDBString. En primer lugar, es la forma en que se ha hecho desde que la piedra envejece, y en segundo lugar, es más fácil depurar las sentencias sql ya que se ve la consulta excact que se ejecuta en la base de datos.
Por lo que entonces. Mi pregunta es si realmente es suficiente usar la función SafeDBString para evitar los ataques de inyección sql. He estado tratando de encontrar ejemplos de código que rompa esta medida de seguridad, pero no puedo encontrar ningún ejemplo de ello.
¿Hay alguien afuera que pueda romper esto? ¿Como lo harias?
EDITAR: Para resumir las respuestas hasta el momento:
- Nadie ha encontrado una forma de evitar SafeDBString en Sql Server 2005 o 2008. Eso es bueno, creo?
- Varias respuestas señalaron que obtiene una ganancia de rendimiento cuando usa consultas parametrizadas. La razón es que los planes de consulta se pueden reutilizar.
- También aceptamos que el uso de consultas parametrizadas proporciona un código más legible que es más fácil de mantener
- Además, es más fácil usar siempre parámetros que usar varias versiones de SafeDBString, cadenas para conversión de números y conversiones de cadena a fecha.
- Al usar los parámetros obtienes la conversión de tipo automática, algo que es especialmente útil cuando trabajamos con fechas o números decimales.
- Y finalmente: no intentes hacer la seguridad tú mismo como escribió JulianR. Los proveedores de bases de datos gastan mucho tiempo y dinero en seguridad. No hay forma de que podamos hacerlo mejor y no hay razón por la que deberíamos intentar hacer su trabajo.
Entonces, aunque nadie fue capaz de romper la seguridad simple de la función SafeDBString, obtuve muchos otros buenos argumentos. ¡Gracias!
Aquí hay algunas razones para usar consultas parametrizadas:
- Seguridad: la capa de acceso a la base de datos sabe cómo eliminar o escapar elementos que no están permitidos en los datos.
- Separación de inquietudes: mi código no es responsable de transformar los datos en un formato que le gusta a la base de datos.
- Sin redundancia: no necesito incluir un ensamblado o clase en cada proyecto que hace esta base de datos formateando / escapando; está integrado en la biblioteca de la clase.
Aquí hay un par de artículos que pueden ser útiles para convencer a sus compañeros de trabajo.
http://www.sommarskog.se/dynamic_sql.html
http://unixwiz.net/techtips/sql-injection.html
Personalmente, prefiero no permitir que ningún código dinámico toque mi base de datos, requiriendo que todos los contactos sean a través de sps (y no uno que use SQl dinámico). Esto significa que nada de lo que he dado permiso a los usuarios para hacer puede hacerse y que los usuarios internos (excepto los pocos con acceso de producción para fines administrativos) no pueden acceder directamente a mis tablas y crear estragos, robar datos o cometer fraudes. Si ejecuta una aplicación financiera, esta es la forma más segura de hacerlo.
Con consultas parametrizadas, obtienes más que protección contra la inyección sql. También obtienes un mejor potencial de almacenamiento en caché del plan de ejecución. Si utiliza el generador de perfiles de servidor sql, aún puede ver el ''sql exacto que se ejecuta en la base de datos'', por lo que tampoco está perdiendo nada en términos de depuración de sus sentencias sql.
Creo que la respuesta correcta es:
No intentes hacer la seguridad tú mismo . Utilice la biblioteca estándar de la industria de confianza disponible para lo que está tratando de hacer, en lugar de tratar de hacerlo usted mismo. Cualquier suposición que haga sobre la seguridad, podría ser incorrecta. Por muy seguro que parezca su enfoque (y parece inestable en el mejor de los casos), existe el riesgo de que no tenga en cuenta algo y realmente desea aprovechar esa oportunidad en lo que respecta a la seguridad.
Usa parámetros
Desde el muy corto tiempo que he tenido para investigar los problemas de inyección de SQL, puedo ver que hacer un valor ''seguro'' también significa que estás cerrando la puerta a situaciones en las que podrías querer apóstrofos en tus datos, ¿qué tal el nombre de alguien? , por ejemplo O''Reilly.
Eso deja parámetros y procedimientos almacenados.
Y sí, siempre debes tratar de implementar el código de la mejor manera que conoces ahora, no solo cómo se hizo siempre.
El argumento es un no-ganar. Si logra encontrar una vulnerabilidad, sus compañeros de trabajo simplemente cambiarán la función SafeDBString para dar cuenta de ello y luego le pedirán que demuestre que no es seguro de nuevo.
Dado que las consultas parametrizadas son una mejor práctica de programación indiscutible, la carga de la prueba debe recaer en ellos para indicar por qué no están utilizando un método que sea tanto más seguro como de mejor rendimiento.
Si el problema es reescribir todo el código heredado, el compromiso más fácil sería usar consultas parametrizadas en todos los códigos nuevos, y refactorizar el código antiguo para usarlos cuando trabaje con ese código.
Creo que el verdadero problema es el orgullo y la terquedad, y no hay mucho más que puedas hacer al respecto.
En primer lugar, su muestra para la versión "Reemplazar" es incorrecta. Necesitas poner apóstrofes alrededor del texto:
string sql = "SELECT * FROM Users WHERE Name=''" + SafeDBString(name) & "''";
SqlCommand getUser = new SqlCommand(sql, connection);
Esa es otra cosa que los parámetros hacen por usted: no necesita preocuparse acerca de si un valor debe estar entre comillas o no. Por supuesto, puede incorporar eso en la función, pero luego debe agregar mucha complejidad a la función: cómo saber la diferencia entre ''NULL'' como nulo y ''NULL'' como solo una cadena, o entre un número y una cadena que simplemente contiene muchos dígitos. Es solo otra fuente de errores.
Otra cosa es el rendimiento: los planes de consulta parametrizados a menudo se almacenan en caché mejor que los planes concatenados, por lo que tal vez guarden al servidor un paso al ejecutar la consulta.
Además, escaparse de comillas simples no es lo suficientemente bueno. Muchos productos DB permiten métodos alternativos para escapar de caracteres que un atacante podría aprovechar. En MySQL, por ejemplo, también puede escapar una comilla simple con una barra diagonal inversa. Y entonces el siguiente valor de "nombre" volaría MySQL solo con la función SafeDBString()
, porque cuando SafeDBString()
la SafeDBString()
, la barra invertida sigue escapando de la segunda, dejando la segunda "activa":
x / ''O 1 = 1; -
Además, JulianR trae a colación un buen punto a continuación: NUNCA intente hacer el trabajo de seguridad usted mismo. Es muy fácil obtener una programación de seguridad incorrecta de maneras sutiles que parecen funcionar, incluso con pruebas exhaustivas. Luego pasa el tiempo y un año después descubres que tu sistema se rompió hace seis meses y nunca lo supiste hasta ese momento.
Siempre confíe tanto como sea posible en las bibliotecas de seguridad proporcionadas para su plataforma. Serán escritos por personas que tienen un código de seguridad para ganarse la vida, mucho más probados que lo que puede administrar, y atendidos por el proveedor si se encuentra una vulnerabilidad.
Entonces yo diría:
1) ¿Por qué intentas volver a implementar algo que está integrado? está ahí, fácilmente disponible, fácil de usar y ya depurado a escala global. Si se encuentran errores en el futuro, se arreglarán y estarán disponibles para todos muy rápidamente sin que tengas que hacer nada.
2) ¿Qué procesos existen para garantizar que nunca se pierda una llamada a SafeDBString? Perderlo en solo 1 lugar podría abrir una gran cantidad de problemas. ¿Cuánto va a mirar estas cosas, y considerar cuánto desperdicia ese esfuerzo cuando la respuesta correcta aceptada es tan fácil de alcanzar.
3) Cuán seguro es que ha cubierto todos los vectores de ataque que Microsoft (el autor de la base de datos y la biblioteca de acceso) conoce en su implementación de SafeDBString ...
4) ¿Qué tan fácil es leer la estructura del sql? El ejemplo usa + concatenación, los parámetros son muy parecidos a string.Format, que es más legible.
Además, hay 2 formas de resolver lo que en realidad se ejecutó: pase su propia función LogCommand, una función simple sin problemas de seguridad , o incluso analice un rastro de sql para determinar qué está realmente sucediendo en la base de datos.
Nuestra función LogCommand es simplemente:
string LogCommand(SqlCommand cmd)
{
StringBuilder sb = new StringBuilder();
sb.AppendLine(cmd.CommandText);
foreach (SqlParameter param in cmd.Parameters)
{
sb.Append(param.ToString());
sb.Append(" = /"");
sb.Append(param.Value.ToString());
sb.AppendLine("/"");
}
return sb.ToString();
}
Correcto o incorrecto, nos brinda la información que necesitamos sin problemas de seguridad.
Esto solo es seguro si tiene la garantía de que va a pasar una cadena.
¿Qué pasa si no estás pasando una cadena en algún punto? ¿Qué pasa si pasa solo un número?
http://www.mywebsite.com/profile/?id=7;DROP DATABASE DB
En última instancia se convertiría en:
SELECT * FROM DB WHERE Id = 7;DROP DATABASE DB
Estoy muy de acuerdo con los problemas de seguridad.
Otra razón para usar parámetros es la eficiencia.
Las bases de datos siempre recopilarán su consulta y la almacenarán en caché, luego reutilizarán la consulta en caché (que obviamente es más rápida para solicitudes posteriores). Si usa parámetros, incluso si usa parámetros diferentes, la base de datos reutilizará su consulta almacenada en caché, ya que coincide con la cadena SQL antes de enlazar los parámetros.
Sin embargo, si no enlaza parámetros, la cadena SQL cambia en cada solicitud (que tiene diferentes parámetros) y nunca coincidirá con lo que está en su caché.
He usado ambos enfoques para evitar ataques de inyección SQL y definitivamente prefiero las consultas parametrizadas. Cuando he utilizado consultas concatenadas he utilizado una función de biblioteca para escapar de las variables (como mysql_real_escape_string) y no estoy seguro de haber cubierto todo en una implementación patentada (como parece que es tu caso).
Hubo pocas vulnerabilidades (no recuerdo qué base de datos era) que están relacionadas con el desbordamiento del búfer de la declaración SQL.
Lo que quiero decir es que SQL-Injection es más que solo "escapar de la cita", y no tienes idea de lo que vendrá después.
No puede realizar fácilmente ninguna verificación de tipo de la entrada del usuario sin usar parámetros.
Si usa las clases SQLCommand y SQLParameter para hacer que usted sea una llamada a base de datos, aún puede ver la consulta SQL que se está ejecutando. Mire la propiedad CommandText de SQLCommand.
Siempre soy un poco sospechoso del enfoque "roll-your-own" para evitar la inyección de SQL cuando las consultas parametrizadas son tan fáciles de usar. En segundo lugar, solo porque "siempre se haya hecho así" no significa que sea la forma correcta de hacerlo.
No vi ningún otro remitente que abordara este lado del ''por qué hacerlo tú mismo es malo'', pero considera un ataque SQL Truncation .
También existe la función QUOTENAME
T-SQL que puede ser útil si no puede convencerlos de usar params. Captura mucho (¿todo?) De las preocupaciones de qoute escapadas.
Otra consideración importante es hacer un seguimiento de los datos escapados y no escamados. Hay toneladas y toneladas de aplicaciones, web y de otro tipo, que no parecen realizar un seguimiento adecuado de cuándo los datos están en bruto: Unicode, y -encriptados, formateados HTML, etcétera. Es obvio que será difícil hacer un seguimiento de qué cadenas están codificadas y cuáles no.
También es un problema cuando terminas cambiando el tipo de alguna variable, quizás solía ser un número entero, pero ahora es una cadena. Ahora tienes un problema.
Por las razones ya dadas, los parámetros son una muy buena idea. Pero odiamos usarlos porque la creación del parámetro y la asignación de su nombre a una variable para su uso posterior en una consulta es un accidente de triple indirección.
La siguiente clase envuelve el generador de cadenas que usará comúnmente para crear solicitudes SQL. Te permite escribir consultas paramaterizadas sin tener que crear un parámetro , para que puedas concentrarte en el SQL. Tu código se verá así ...
var bldr = new SqlBuilder( myCommand );
bldr.Append("SELECT * FROM CUSTOMERS WHERE ID = ").Value(myId, SqlDbType.Int);
//or
bldr.Append("SELECT * FROM CUSTOMERS WHERE NAME LIKE ").FuzzyValue(myName, SqlDbType.NVarChar);
myCommand.CommandText = bldr.ToString();
La legibilidad del código, espero que esté de acuerdo, ha mejorado mucho, y la salida es una consulta parametrizada adecuada.
La clase se ve así ...
using System;
using System.Collections.Generic;
using System.Text;
using System.Data;
using System.Data.SqlClient;
namespace myNamespace
{
/// <summary>
/// Pour le confort et le bonheur, cette classe remplace StringBuilder pour la construction
/// des requêtes SQL, avec l''avantage qu''elle gère la création des paramètres via la méthode
/// Value().
/// </summary>
public class SqlBuilder
{
private StringBuilder _rq;
private SqlCommand _cmd;
private int _seq;
public SqlBuilder(SqlCommand cmd)
{
_rq = new StringBuilder();
_cmd = cmd;
_seq = 0;
}
//Les autres surcharges de StringBuilder peuvent être implémenté ici de la même façon, au besoin.
public SqlBuilder Append(String str)
{
_rq.Append(str);
return this;
}
/// <summary>
/// Ajoute une valeur runtime à la requête, via un paramètre.
/// </summary>
/// <param name="value">La valeur à renseigner dans la requête</param>
/// <param name="type">Le DBType à utiliser pour la création du paramètre. Se référer au type de la colonne cible.</param>
public SqlBuilder Value(Object value, SqlDbType type)
{
//get param name
string paramName = "@SqlBuilderParam" + _seq++;
//append condition to query
_rq.Append(paramName);
_cmd.Parameters.Add(paramName, type).Value = value;
return this;
}
public SqlBuilder FuzzyValue(Object value, SqlDbType type)
{
//get param name
string paramName = "@SqlBuilderParam" + _seq++;
//append condition to query
_rq.Append("''%'' + " + paramName + " + ''%''");
_cmd.Parameters.Add(paramName, type).Value = value;
return this;
}
public override string ToString()
{
return _rq.ToString();
}
}
}
Se puede romper, sin embargo, el medio depende de versiones / parches exactos, etc.
Uno que ya ha sido mencionado es el error de desbordamiento / truncamiento que puede ser explotado.
Otro medio futuro sería encontrar errores similares a otras bases de datos; por ejemplo, la pila MySQL / PHP sufrió un problema de escape porque ciertas secuencias UTF8 podrían usarse para manipular la función de reemplazo; la función de reemplazo sería engañada para introducir los caracteres de inyección.
Al final del día, el mecanismo de seguridad de reemplazo se basa en la funcionalidad esperada pero no prevista . Dado que la funcionalidad no era el propósito previsto del código, existe una alta probabilidad de que alguna rareza descubierta rompa la funcionalidad esperada.
Si tiene una gran cantidad de código heredado, el método de reemplazo podría utilizarse como un recurso provisional para evitar una reescritura y pruebas prolongadas. Si está escribiendo un nuevo código, no hay excusa.
Siempre use consultas parametrizadas donde sea posible. A veces, incluso una entrada simple sin el uso de caracteres extraños ya puede crear una inyección SQL si no se identifica como una entrada para un campo en la base de datos.
Así que simplemente deje que la base de datos haga su trabajo de identificación de la entrada en sí misma, sin mencionar que también ahorra un montón de problemas cuando necesita insertar caracteres extraños que de otra manera se escaparían o cambiarían. Incluso puede ahorrar un valioso tiempo de ejecución al final por no tener que calcular la entrada.
Usaría procedimientos almacenados o funciones para todo, por lo que la pregunta no surgiría.
Donde tengo que poner SQL en el código, uso parámetros, que es lo único que tiene sentido. Recuérdeles a los disidentes que hay hackers más inteligentes de lo que son, y con un mejor incentivo para romper el código que está tratando de burlarlos. Usando parámetros, simplemente no es posible, y no es como si fuera difícil.
Y luego alguien usa "en lugar de". Los parámetros son, IMO, la única forma segura de hacerlo.
También evita muchos problemas de i18n con fechas / números; ¿qué fecha es 01/02/03? ¿Cuánto es 123,456? ¿Sus servidores (servidor de aplicación y servidor de base de datos) están de acuerdo el uno con el otro?
Si el factor de riesgo no es convincente para ellos, ¿qué hay del rendimiento? El RDBMS puede volver a utilizar el plan de consulta si usa parámetros, lo que ayuda al rendimiento. No puede hacer esto solo con la cadena.
2 años después , recidivé ... Cualquiera que encuentre un dolor de parámetros es bienvenido para probar mi Extensión VS, QueryFirst . Edita su solicitud en un archivo .sql real (Validación, Intellisense). Para agregar un parámetro, simplemente escríbalo directamente en su SQL, comenzando con ''@''. Cuando guarde el archivo, QueryFirst generará clases de contenedor para permitirle ejecutar la consulta y acceder a los resultados. Buscará el tipo de base de datos de su parámetro y lo correlacionará con un tipo .net, que encontrará como una entrada a los métodos Execute () generados. No podría ser más simple . Hacerlo de la manera correcta es radicalmente más rápido y más fácil que hacerlo de otra manera, y la creación de una vulnerabilidad de inyección sql se vuelve imposible, o al menos perversamente difícil. Existen otras ventajas importantes, como la posibilidad de eliminar columnas en su base de datos y ver inmediatamente errores de compilación en su aplicación.
descargo de responsabilidad legal: escribí QueryFirst