Bugs y Exploits‎ > ‎Nivel Web‎ > ‎Inyección SQL‎ > ‎

Protegerse de un ataque SQL Injection

Muchos ataques se podrían evitar usando PDO. El método es usar prepared statements y parameterized queries. Esto son sentencias SQL preparadas que se envían a la base de datos de forma separada a cualquier parámetro. De esta forma podríamos prevenir este ataque SQLi (al menos con los payload más sencillos).

Pero Incluso los métodos nativos que existen en PHP para sanear las entradas de usuario (como mysql_real_escape_string) pueden presentar (raros) problemas y fallar en algunos casos como cuando se usan codificación de caracteres diferentes a UTF-8 junto a versiones no actualizadas de MySQL 

Inyección de SQL es una técnica para tomar el control de una consulta de base de datos que a menudo resulta en un compromiso de confidencialidad. En algunos casos (por ejemplo, si SELECT 'código diabólico aquí' INTO OUTFILE '/var/www/reverse_shell.php' tiene éxito) esto puede resultar en una toma de posesión completa del servidor.

Dado que la inyección de código (que abarca las técnicas de SQL, LDAP, Command OS y XPath Injection) ha permanecido constantemente en la parte superior de las vulnerabilidades de Top 10 de OWASP, es un tema popular para los bloggers que intentan mojarse en el campo de seguridad de aplicaciones.

Usa sentencias preparadas y consultas parametrizadas

Aunque se podrían sanear las entradas usando métodos como mysqli_real_escape_string, es más recomendable la utilización de sentencias preparadas o parametrizadas. Las sentencias preparadas te permitirán ejecutar la misma sentencia con gran eficiencia.

En PHP, tienes dos alternativas principales: PDO y MySQLi. Hay varias diferencias entre ambas, pero la principal es que PDO se puede usar con diferentes tipos de base de datos (dependiendo del driver utilizado) mientras que MySQLi es exclusivamente para bases de datos MySQL. Es por ello que recomendaría PDO sobre MySQLi.

PDO

Los marcadores de posición (que indican dónde se sustituirá una cadena por su valor), se pueden definir bien usando un signo de interrogación (?) o bien usando un nombre (generalmente empezando con :). Personalmente prefiero usar un nombre, porque eso me ayuda a encontrar posibles errores en caso de tener múltiples variables.

Aquí dejo un ejemplo para el código de la pregunta:

// la variable $pdo contendrá el objeto con la conexión PDO
$pdo = new PDO('mysql:host=mihost;dbname=basedatos', "usuario", "contraseña");

$id_usuario = $_POST["id"];

$sentencia = $pdo->prepare("SELECT * FROM usuarios WHERE id = :idusuario");
$sentencia=$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$sentencia->bindParam(":idusuario", $id_usuario, PDO::PARAM_INT);
$sentencia->execute();

En este caso, :idusuario se sustituirá por el valor de $_POST["id"] de forma segura, y cuando hace el bind se indica que la variable es de tipo entero (PDO::PARAM_INT).

Nota: si la variable es una cadena de texto se usará PDO::PARAM_STR y no hace falta poner las comillas en la sentencia SQL; al especificarle a PHP que es una cadena, las añadirá automáticamente al hacer el bind.

En caso de que existan varias variables a incluir en la sentencia SQL, se debe incluir un único parámetro para cada uno de los valores que se usan en la sentencia. Del ejemplo anterior, el :idusuario puede usarse una única vez en la consulta que se esta preparando. Si fuera necesario usar el "idusuario" de nuevo en la consulta, se debe crear otro parámetro con el valor de $id_usuario.

$pdo = new PDO('mysql:host=mihost;dbname=basedatos', "usuario", "contraseña");

$id_usuario = $_POST["id"];

$sentencia = $pdo->prepare("UPDATE usuarios SET id = :idusuario WHERE id = :idusuario1");
$sentencia=$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$sentencia->bindParam(":idusuario", $id_usuario, PDO::PARAM_INT);
$sentencia->bindParam(":idusuario1", $id_usuario, PDO::PARAM_INT);
$sentencia->execute();

MySQLi

Este método tiene dos interfaces: una procedural y otra orientada a objetos. La interfaz procedural es muy parecida a mysql_*, y por ello la gente que migra desde mysql_* puede sentirse atraída por la facilidad que mysqli_* ofrece. Aunque, de nuevo personalmente, optaría por la versión POO.

Nota: aunque las funciones mysqli_* suelen ser parecidas a las mysql_*, en algunos casos pueden tener diferentes parámetros de entrada o diferentes salidas, lo que puede llevar a algo de confusión al principio.

El ejemplo de la pregunta quedaría así con MySQLi en su interfaz orientada a objetos:

// en $mysqli tendremos la conexión MySQLi
$mysqli = new mysqli("mihost", "usuario", "contraseña", "basedatos");

$id_usuario = $_POST["id"];

$sentencia = $mysqli->prepare("SELECT * FROM usuarios WHERE id = ?");
$sentencia->bind_param("i", $id_usuario );
$sentencia->execute();

Como se puede ver, es bastante parecido a PDO (cambia un poco cómo se especifica el tipo de valor, i para enteros y s para cadenas, pero la idea es similar).

En la versión procedural de MySQLi, el código equivalente sería:

// en $conn tendríamos la conexión a la base de datos con MySQLi
$conn = mysqli_connect("mihost", "usuario", "contraseña", "basedatos");

$id_usuario = $_POST["id"];

$sentencia = mysqli_prepare("SELECT * FROM usuarios WHERE id = ?");
mysqli_stmt_bind_param($sentencia, "i", $id_usuario);
mysqli_stmt_execute($sentencia);


Existen ciertos principios a considerar para proteger nuestras aplicaciones de un SQL Injection:

  1. No confiar en la entrada del usuario.
  2. No utilizar sentencias SQL construidas dinámicamente.
  3. No utilizar cuentas con privilegios administrativos.
  4. No proporcionar mayor información de la necesaria.

A continuación veremos algunos ejemplos de como implementar dichos principios.

1. No confiar en la entrada del usuario significa:

  • Filtrar la entrada del usuario de caracteres SQL para limitar los caracteres involucrados en un SQL Injection.
Private Function string SafeSqlLiteral( _
ByVal inputSQL As String) As String
Return inputSQL.Replace("'", "''")
End Function
'...
Dim safeSQL As String = SafeSqlLiteral(Login.Text)
Dim myCommand As SqlDataAdapter = _
New 
SqlDataAdapter("SELECT au_lname, au_fname " & _
"FROM authors WHERE au_id = '" & safeSQL & "'", _
myConnection)
  • Proteger las instrucciones de búsqueda de modelos coincidentes (LIKE).
Private Function SafeSqlLikeClauseLiteral( _
ByVal
 inputSQL As String) As String
Dim As String = inputSQL
s = inputSQL.Replace("'", "''")
s = s.Replace("[", "[[]")
s = s.Replace("%", "[%]")
s = s.Replace("_", "[_]")
Return s
End Function
  • Verificar tanto el tamaño y como el tipo de los datos de las entradas de usuario:
    • Evitando los siguientes tipos caracteres de riesgo para el gestor de datos:
      • El delimitador de consultas: Punto y coma (;)
      • Delimitador de datos de tipo cadena de caracteres: Comilla sencilla (').
      • Delimitadores de cometario: Guión doble (--) y "/*..*/" en el caso de SQL Server.
  • Nota: En lugar de evitar los caracteres peligrosos, otro modo de protegernos es aceptar sólo los caracteres inofensivos.
    • Evitando las cadenas con el inicio de nombres de las tablas y los procedimientos del sistema: "sys" y "xp_" en el caso de SQL Server. Así como las siguientes palabras: AUX, CLOCK$, COM1, COM8, CON, CONFIG$, LPT1, LPT8, NUL y PRN
    • Utilizar de preferencia controles con valores predefinidos o discretos tales como cuadros de lista, cuadros combinados, de verificación, etc. en lugar de cuadros de texto.
  • Verificar cualquier tipo de entrada, no sólo lo introducido en los controles IU sino también aquellas que no son visibles, como parámetros de entrada y campos tipo hidden de las páginas web.
  • Realizar la verificación en todos los niveles y capas de la aplicación, ya que si sólo protegemos la capa de presentación somos vulnerables a que un atacante salte a la siguiente capa y realizar su ataque.

2. No utilizar sentencias SQL construidas dinámicamente. En lugar de ello:

  • Utilizar instrucciones SQL con Parámetros.
Dim myCommand As SqlDataAdapter = New SqlDataAdapter( _
"SELECT au_lname, au_fname FROM Authors " & _
"WHERE au_id= @au_id", myConnection)
Dim parm As 
SqlParameter = _
myCommand.SelectCommand.Parameters.Add("@au_id", _
SqlDbType.VarChar, 11)
parm.Value = Login.Text

Aunque de hecho es mejor utilizar Procedimientos Almacenados siempre que sea posible, así como también:

  • Utilizar Parámetros al llamar Procedimientos Almacenados.
Dim myCommand As SqlDataAdapter = _
New
 SqlDataAdapter("AuthorLogin", myConnection)
myCommand.SelectCommand.CommandType = _
CommandType.StoredProcedure
Dim parm As 
SqlParameter = _
myCommand.SelectCommand.Parameters.Add("@LoginId", _
SqlDbType.VarChar,11)
parm.Value = Login.Text

3. No utilizar cuentas con privilegios administrativos.

  • Ejecutar las sentencias SQL o invocar Procedimientos Almacenados con una cuenta con privilegios mínimos. Nunca emplear 'sa' en el caso de MS SQL Server. Pero de preferencia además...
  • Conceder permisos de ejecución únicamente a Procedimientos Almacenados propios desde los cuales, a manera de "wraper", se realicen las consultas a las Tablas de Usuario y llamados a los Procedimientos Almacenados del Sistema que se requieran en las aplicaciones, y negar el acceso directo a éstos últimos y a las Tablas de Usuario.

4. No proporcionar mayor información de la necesaria.

  • No exponer al usuario final los mensajes de error devueltos por el gestor de la base de datos, para no brindar mayor información que sea útil al atacante.
  • Implementar un sistema de gestión de errores que notifique del mismo únicamente a los administradores de la aplicación y el gestor de la base de datos. Por ejemplo, para el caso de una aplicación web, establecer en el archivo de configuración web (Web.Config) el valor del atributo debug del elemento compilation en False y el atributo mode del elemento customErrors en On o en su defecto en RemoteOnly.
<compilation defaultLanguage="vb" debug = "false" />
<customErrors mode="RemoteOnly" 
defaultRedirect="GenericErrorPage.htm">
</customErrors>

Conclusión.

Para prevenir un ataque SQL Injection, así como para realizar pruebas (Testing) de vulnerabilidad contra el mismo en nuestras aplicaciones, no debemos olvidar que cualquier aplicación que permita una "entrada" que sirva de parámetro para una consulta SQL es vulnerable a éste ataque. Por ello en éste artículo describimos brevemente de lo que se trata un SQL Injection, así como de los principios básicos a seguir para protegernos del mismo. Por último ejemplificamos algunas implementaciones de dichos principios. Medidas a tomar para protegerse de un ataque SQL Injection.

Fuente original: http://willyxoft.wordpress.com/articulos/sql-injection/  por Willy Mejia (WillyXoft)
Comments