Skip to main content

Cómo corregir la inyección SQL: un ORM no es suficiente

Escrito por
Fixing SQL Injection ORM is not enough tumb

8 de junio de 2016

0 minutos de lectura

Uno de los tipos de vulnerabilidad más peligrosos y extendidos es la inyección SQL, que permite a los atacantes acceder a tu base de datos de backend. Usar sentencias preparadas y mapeo objeto-relacional (ORM) es una buena forma de defenderte contra la inyección SQL, pero no es suficiente. Como muestra esta publicación, los paquetes ORM, como Sequelize y MySQL, pueden tener fallas que te dejen expuesto. Para protegerte de verdad, tienes que hacer más.

En nuestro informe State of Open Source Security Report 2019, descubrimos que las vulnerabilidades de inyección SQL siguen siendo una fuente frecuente de preocupación en materia de seguridad, con un pico de 16 vulnerabilidades detectadas en bibliotecas del repositorio PHP Packagist.

Resumen

Una vulnerabilidad de inyección SQL, o SQLi, permite que un atacante agregue o «inyecte» texto sin estructura en un comando SQL, lo que provoca consecuencias no deseadas. Una forma de protegerte contra la inyección SQL es usar un paquete ORM, que convierte tus objetos y las acciones que realizas sobre ellos en SQL. Al usar estos paquetes, nunca escribes SQL directamente, así que no tienes oportunidad de equivocarte y permitir inyecciones maliciosas. ¡Genial!

Sin embargo, es fácil olvidar que, aunque tú no escribas SQL, eso no significa que nadie lo haga. De hecho, los paquetes ORM de npm todavía tienen que convertir estas acciones en SQL. Estos paquetes, como todos los demás, son software, y el software puede tener errores. Durante el último año, se reportaron 4 vulnerabilidades de inyección SQL en los dos paquetes ORM más populares de npm: sequelize y node-mysql. Así, esta preocupación pasó de ser teórica a ser real. Estos problemas ya se corrigieron en las versiones más recientes de los paquetes, pero quizá estés usando versiones antiguas, y podrían aparecer nuevas vulnerabilidades.

Esta publicación presenta un par de ejemplos de estas fallas y explica qué otras capas de defensa debes aplicar. Las principales son:

Antes de analizar los escenarios problemáticos, repasemos brevemente qué es la inyección SQL, qué opciones tienes para protegerte y qué es un ORM.

Ejemplo sencillo de inyección SQL

Imagina una aplicación de lista de tareas con una función de búsqueda que encuentra los elementos que contienen un texto determinado. La búsqueda se realiza mediante una consulta a la base de datos, implementada con el paquete sequelize. Suponiendo que la conexión a la base de datos ya está configurada, esta sería la función de búsqueda del backend:

function findItems(req, resp)
{
  try {
    // Find the relevant items
    sequelize.query(
      "SELECT Desc FROM Items WHERE Desc like ('%" + req.params.snippet + "%')",
      { type: sequelize.QueryTypes.SELECT}
    ) // Retrieve results
    .spread(function(results, metadata) {
        // Add results to response
     });
  } catch {
    // Handle error
  }
}

En un caso de uso legítimo, el usuario ingresará un valor sencillo (por ejemplo, Buy), que se traducirá en la siguiente consulta legítima y devolverá todas las tareas de compras pertinentes de nuestra lista:

SELECT Desc FROM Items WHERE Desc like ('%Buy%')

Sin embargo, un atacante podría enviar deliberadamente un apóstrofo (') para salir del contexto de la cadena previsto e ingresar en la consulta. Por ejemplo, el atacante podría ingresar el valor ') UNION SELECT username||'_'||password FROM Users --, lo que daría como resultado la siguiente consulta:

SELECT Desc FROM Items WHERE Desc like ('%') UNION SELECT username||'_'||password FROM Users -- %')

Suponiendo que tenemos una tabla Users con los campos password y username, la consulta anterior agregará la lista completa de nombres de usuario y contraseñas a nuestra lista de tareas pendientes. El resto del texto se puede comentar fácilmente con el comando --, simplemente para mantener intacta la consulta SQL.

Esta implementación vulnerable puede parecer especialmente ingenua, pero sus variantes son bastante frecuentes. En todos los casos, se agrega algún tipo de entrada de usuario sin validar a una consulta SQL sin procesar, lo que permite salir del contexto original (por ejemplo, una cadena) y ejecutar acciones imprevistas. Por otro lado, el ataque que mostramos fue bastante sencillo y preciso, pero los atacantes reales envían gradualmente muchas variantes distintas y solo necesitan que una funcione.

Impacto y corrección

La inyección SQL es una vulnerabilidad extremadamente grave. En la mayoría de los casos, una sola inyección SQL en cualquier parte de tu sitio web puede llegar a permitir la ejecución de cualquier consulta en la base de datos y la extracción y manipulación de sus datos. Como las bases de datos suelen contener la información más confidencial del sistema, permitir que los atacantes accedan a ella es devastador.

Hay dos técnicas principales, que no son mutuamente excluyentes, para prevenir la inyección SQL: validar las entradas y usar sentencias preparadas.

Validación de entradas

La inyección SQL, al igual que otros ataques de inyección, comienza con una entrada maliciosa del usuario. Por eso, una buena forma de prevenirla es verificar que la entrada que proporciona el usuario sea válida. Si no lo es, podemos cancelar por completo la operación o eliminar los caracteres potencialmente peligrosos.

La validación de entradas puede realizarse con un modelo de seguridad negativo o positivo.

Un modelo de seguridad negativo consiste en prohibir caracteres o patrones peligrosos específicos. Por ejemplo, en el caso anterior, podríamos prohibir el apóstrofo que permitió salir del contexto de la cadena. Por desgracia, SQL es complejo, así que es difícil encontrar todos los caracteres potencialmente peligrosos. En este ejemplo, también tendríamos que bloquear caracteres como retroceso (b), barra invertida (\), nulo (x00) y probablemente varios más. Además, la lista se alarga si admitimos varios tipos de bases de datos. Aun así, bloquear los caracteres peligrosos es una técnica de mitigación bastante eficaz y sencilla.

Un modelo de seguridad positivo consiste en permitir únicamente caracteres específicos. En el ejemplo anterior, limitar la entrada a letras, dígitos y espacios (/a-zA-Z0-9 /) habría eliminado el riesgo de forma eficaz. Este enfoque, también llamado lista de permitidos, suele ser mejor desde el punto de vista de la seguridad, ya que evita sorpresas por valores que no habías considerado. Sin embargo, es más probable que bloquee caracteres legítimos, sobre todo al ampliar la compatibilidad con conjuntos de caracteres Unicode.

Sentencias preparadas y ORM

Si los ataques de inyección SQL comienzan con la entrada, terminan con la consulta SQL, lo que nos da una segunda oportunidad para evitar el problema. En este punto, la raíz del problema es la simple concatenación de cadenas que se usa para crear la consulta SQL. En cambio, si usáramos una plantilla para la consulta, podríamos indicarle a la base de datos (o a la biblioteca de conexión) que se trata de un valor de cadena y dejar que lo codifique según sea necesario. Esta es la solución que mencionamos al principio de esta publicación.

Estas plantillas se conocen como «sentencias preparadas» o, a veces, «sentencias parametrizadas». El paquete sequelize que usamos antes también las admite, así que podríamos corregir la vulnerabilidad modificando nuestra función de esta manera:

function findItems(req, resp)
{
  try {
    // Find the relevant items
    sequelize.query(
      "SELECT Desc FROM Items WHERE Desc like ?",
      { replacements: ['%'+req.params.snippet+'%'],
        type: sequelize.QueryTypes.SELECT }
    ) // Retrieve results
    .spread(function(results, metadata) {
        // Add results to response
     });
  } catch {
    // Handle error
  }
}

Sequelize entenderá que ? representa un valor y no un comando SQL, y lo codificará adecuadamente para impedir intentos de escape.

Las sentencias preparadas no solo son una buena forma de proteger tu código, sino también de hacerlo más legible y fácil de mantener. En otras palabras, cuando sientas la tentación de concatenar valores en una sentencia SQL, resiste el impulso y usa una sentencia preparada.

Un paso más allá: un enfoque aún más programático para SQL consiste en usar el mapeo objeto-relacional (ORM). Usar un ORM significa mapear las tablas de tu base de datos a tus objetos, lo que te permite leer, escribir y consultar objetos completos. Como los ORM reducen aún más el uso de SQL explícito, también son una buena forma de evitar la inyección SQL.

Cuando los ORM son vulnerables: Sequelize

Tanto las sentencias preparadas como los ORM son buenas formas de delegar la responsabilidad de la codificación en los «expertos»: los paquetes especializados en esa tarea. Sin embargo, ser experto no significa que nunca se cometan errores… Como mencionamos al principio, el año pasado se demostró con 4 vulnerabilidades de inyección SQL en dos de los paquetes ORM más populares de npm: sequelize y node-mysql.

Las vulnerabilidades se debieron sistemáticamente a parámetros sin validar en varias llamadas de ORM y sentencias preparadas. Al fin y al cabo, estas llamadas a funciones de ORM y sentencias preparadas todavía tienen que traducir los parámetros a una sentencia SQL, y podrían olvidar escaparlos o validarlos al hacerlo.

Veamos un par de vulnerabilidades de sequelize para entender mejor lo que ocurrió. Cabe señalar que estas vulnerabilidades ya se corrigieron y que los desarrolladores de Sequelize respondieron rápidamente cuando se detectaron y reportaron los problemas. Puedes encontrar la lista completa de vulnerabilidades de Sequelize en la base de datos de vulnerabilidades de Snyk, junto con instrucciones para corregirlas.

La primera, divulgada en enero de 2016 y corregida en la versión 3.17.0, afectaba a la función findAll, que suele usarse para consultar objetos de la base de datos con ORM. Al convertir sus parámetros a SQL, la función no restringía los valores del parámetro LIMIT, lo que abría la puerta a una vulnerabilidad de inyección SQL.

Este es un ejemplo de cómo se podría activar esta vulnerabilidad en una lista de tareas pendientes.

models.Items.findAll({
  limit: '1; DELETE FROM Items WHERE 1=1; --',
}).then(function (users) {
  console.log(users);
});

Suponiendo que Items contiene los campos Username y Desc, la consulta resultante se vería más o menos así:

SELECT Username,Desc FROM Items LIMIT 1; DELETE FROM Items WHERE 1=1; --

En marzo de 2016 se divulgó otra omisión similar. Esta vez, la falla se produjo al crear sentencias preparadas y concatenar valores para la sentencia IN. Este es un ejemplo de ataque:

db.query('SELECT Desc FROM Items WHERE Username IN (:names)', {
  replacements: {
    names: ["Bobby", "'); DELETE FROM Items WHERE 1=1; --')"]
  }
});

Una vez detectadas, estas vulnerabilidades no son demasiado complejas, pero demuestran que incluso los paquetes populares pueden fallar. SQL es complejo y es fácil pasar por alto casos extremos.

Solución: defensa en profundidad

Si antes no quedó claro, lo diré sin rodeos: definitivamente debes usar ORM y sentencias preparadas. Reducen la gran mayoría de los riesgos de inyección SQL y, en general, son buenas prácticas de desarrollo de software. Sin embargo, no debes pensar que estos paquetes te hacen completamente inmune.

En cambio, también debes validar las entradas. Así impedirás que las entradas maliciosas lleguen a tu sistema desde el principio, lo que es una excelente forma de reducir el riesgo. Aplicar varias defensas como esta suele llamarse «defensa en profundidad» y es algo muy positivo. Si un ataque atraviesa una capa de defensa (la validación de entradas), es probable que la segunda lo detenga. Si cada capa permitiera el paso del 1 % de los ataques, dos capas bloquearían todos menos el 0,01 %. Es una buena estrategia.

Además, debes mantenerte al tanto de las vulnerabilidades conocidas en los paquetes que usas y corregirlas en cuanto se detecten. Puedes usar Snyk (¡gratis!) para analizar tus aplicaciones en busca de las vulnerabilidades mencionadas y integrar pruebas de vulnerabilidades en tu proceso de desarrollo para ayudar a mantenerlas libres de vulnerabilidades.

A los desarrolladores les encanta. Los equipos de seguridad confían en él.

Las herramientas de Snyk, diseñadas primero para desarrolladores, ofrecen seguridad integrada y automatizada que satisface tus necesidades de gobernanza y cumplimiento.