Comprender y evitar la inyección SQL

Michael Garrison hace un mes 6 minutos de lectura
Cargando el reproductor AudioNative de texto a voz de Elevenlabs ...

Si ha dedicado tiempo a la gestión de Rock, es probable que haya explorado sus potentes funciones de personalización: creación de páginas personalizadas, informes, cuadros de mando y mucho más. Rock le da acceso completo a sus datos, lo que le permite un sinfín de posibilidades.

Pero este poder también conlleva una responsabilidad. En Parque Jurásico, la famosa cita de Ian Malcolm advierte sobre hacer algo sólo porque podemos, sin plantearnos si debemos. Esto también se aplica a la personalización de Rock: debemos asegurarnos de implementar soluciones de la forma correcta, no solo de la más fácil.

Un ejemplo del riesgo de las soluciones "fáciles" se llama Inyección SQL. Este es un tipo de vulnerabilidad que es sorprendentemente fácil de introducir sin darse cuenta. El término puede sonar técnico e intimidante, pero entenderlo es esencial para todo administrador de rocas. Vamos a desglosarlo.


¿Qué es la inyección SQL?

Imagina esto: estás creando una plantilla que permite a cualquiera de tus miembros ver el nombre y el correo electrónico de su cónyuge. Al fin y al cabo, queremos que vean fácilmente cuándo hay que actualizar algo, ¿no?

Una forma de obtener la información es mediante una sentencia SQL, es decir, accediendo directamente a lo que se pida de la base de datos. Afortunadamente, una consulta SQL básica es relativamente fácil de aprender y la comunidad Rock siempre está dispuesta a contribuir con fragmentos y punteros a cualquiera que pueda explicar la necesidad.

Así que añade un bloque HTML a alguna página pública para empezar a construir la consulta. La codificación de Cindy Decker en la consulta está bien por ahora - la harás dinámica más tarde. Activa el comando SQL y utiliza esta plantilla:

{% sql %}
SELECT
  [NickName]
  , [LastName]
  , [Email]
FROM
  [Person]
WHERE
  [Guid] = 'b71494db-d809-451a-a950-28898d0fd92c';
{% endsql %}

{% for row in results %}
  <p>{{ row.NickName }} {{ row.LastName }}: {{ row.Email }}</p>
{% endfor %}

Siendo un administrador responsable, has evitado usar PersonId ya que es secuencial y predecible, permitiendo a cualquiera recorrer los IDs y extraer información de contacto. En su lugar, utiliza un GUID, que no es fácil de adivinar. La ejecución de la plantilla confirma que los datos de Cindy aparecen cuando se carga la página.

Pero antes de continuar, pregúntese: ¿Podemos confiar en que esta consulta hará siempre lo que pretendemos? Las consultas SQL eluden la seguridad de Rock, permitiendo el acceso total a la base de datos, incluida la capacidad de modificar o eliminar datos. Si alguien con malas intenciones se involucrara, ¿qué le impediría alterar esta consulta para hacer daño?

"Pregúntese: ¿Podemos confiar en que esta consulta haga siempre lo que pretendemos?".

En el caso de esta consulta, queremos asegurarnos de que sólo los administradores de la Roca puedan editar la configuración del bloque. Dado que la consulta se define dentro de la configuración y no depende de entradas externas, podemos confiar en ella, suponiendo que confiamos en todos los que desempeñan esa función. Esto hace que, en general, sea seguro utilizarla en páginas en las que se necesita esta información.

Por supuesto, no queremos que todo el mundo vea la información de Cindy. propio del cónyuge. Para hacerlo dinámico, actualizaremos la consulta para que realice la búsqueda basándose en un parámetro de URL. La URL de esta página podría ser la siguiente: https://www.rocksolidchurchdemo.com/spouseinfo?SpouseGuid=b71494db-d809-451a-a950-28898d0fd92c (no, esta página no existe en demo, pero puedes ver lo que estamos haciendo).

Aquí está la misma plantilla que usamos antes, pero cambiamos el GUID codificado por algo de Lava que obtendrá ese texto de la URL (he resaltado la parte que cambió):

{% sql %}
SELECT
  [NickName]
  , [LastName]
  , [Email]
FROM
  [Person]
WHERE
  [Guid] = '{{ 'Global' | PageParameter:'SpouseGuid' }}';
{% endsql %}
{% for row in results %}
  <p>{{ row.NickName }} {{ row.LastName }}: {{ row.Email }}</p>
{% endfor %}

Ahora que hemos hecho ese cambio, podemos cambiar la URL para proporcionar un GUID válido diferente, y nos mostrará la información de esa persona en su lugar.

Una vez más, debemos preguntarnos ¿Podemos confiar en que esta consulta haga siempre lo que pretendemos?

El uso de un GUID en lugar de un ID ayuda a evitar el robo de datos, pero hemos pasado por alto otro riesgo: estamos permitiendo que un parámetro de URL (que cualquiera puede modificar) forme parte de nuestra consulta. Suponemos que la URL siempre contendrá un GUID válido, pero ¿y si alguien introduce algo inesperado?

Ese es el núcleo de la inyección SQL: tomar datos no fiables y permitir que se conviertan en parte de una consulta.

Siempre corremos el riesgo de sufrir una inyección SQL cuando tomamos datos no fiables y permitimos que formen parte de una consulta.

Considere lo siguiente: ¿qué ocurre si alguien establece SpouseGuid=' O 1=1 --? Puede parecer un galimatías, pero introdúcelo en tu consulta como si fuera el Lava y verás lo que pasa.

Su simple pregunta se convierte ahora en:

SELECT [NickName], [LastName], [Email] FROM [Person]
WHERE [Id] = '' OR 1=1 --'
(El -- inicia un comentario en SQL, evitando errores por el final ').

¡Caramba! Dado que estamos inyectando texto sin formato en la consulta, ese alborotador acaba de secuestrarla para mostrar a todas las personas de la base de datos cuyo Id sea '', o donde 1=1. Como 1 es siempre igual a 1, nuestra consulta está mostrando los datos de contacto de los pastores, del personal... de todo el mundo.

Y la cosa empeora. En lugar de sólo modificar el filtro, ¿qué pasa si se inserta un comando completamente nuevo, como: DROP TABLE [TransacciónFinanciera]?

Eso borraría todos los registros financieros - no sólo un mal día, sino un desastre.


Protección contra la inyección SQL

Ahora que vemos lo trivial y poderosa que puede ser la Inyección SQL, probablemente estés muy nervioso por este riesgo. ¡Así que vamos a hablar acerca de cómo protegerse de que esto suceda!

Opción 1.

La forma más común de evitar este ataque basado en SQL sería... ¡evitar usar SQL! En muchos sitios podría utilizar SQL, podrías obtener los mismos datos utilizando Lava en su lugar. En este caso podrías utilizar un PersonByGuid Filtro Lava para obtener el registro de la persona en lugar de utilizar SQL.

Esto hace lo mismo que la consulta anterior:

{% assign spouse = 'Global' | PageParameter:'SpouseGuid' | PersonByGuid %}
<p>{{ spouse.NickName }} {{ spouse.LastName }}: {{ spouse.Email }}</p>

Como los filtros Lava son de sólo lectura y respetan la seguridad de las entidades, el único riesgo es que alguien adivine u obtenga el GUID de otra persona. Incluso en ese caso, sólo tendría acceso al número de teléfono de esa persona; no hay forma de ampliarlo para ver varios registros.

Opción 2.

En los casos en que el uso del comando SQL es la mejor o la única opción, la forma más segura de manejar la entrada no confiable en su consulta es a través de parámetros. Estos se cubren en la documentación del comando SQL bajo el encabezado Parámetros SQL.

Por ejemplo, en lugar de insertar directamente la entrada del usuario, la asigna como parámetro:

{% assign spouseGuid = 'Global' | PageParameter:'SpouseGuid' %}
{% sql SpouseGuid:'{{ spouseGuid }}' %}
SELECT
  [NickName]
  , [LastName]
  , [Email]
FROM
  [Person]
WHERE
  [Guid] = @SpouseGuid;
{% endsql %}
{% for row in results %}
  <p>{{ row.NickName }} {{ row.LastName }}: {{ row.Email }}</p>
{% endfor %}

Esto funciona porque la entrada no fiable nunca se convierte en parte de la propia consulta. En su lugar, actúa como una referencia, indicando a SQL que devuelva los registros en los que el GUID coincide con el valor dado. La entrada de un atacante no puede escapar de la variable, lo que significa que un intento de inyección como ' O 1=1 -- simplemente devuelve cero resultados.

Opción 3.

La tercera opción, si las dos anteriores no están disponibles, es confiar en el filtro Lava SanitizeSql para hacer segura cualquier entrada que no sea de confianza. Lo ideal sería convertir esos valores en variables SQL o utilizarlo dentro de un parámetro SQL como el anterior.

Como mínimo, si tiene que utilizar Lava directamente en su consulta, tendría el siguiente aspecto WHERE [Guid] = '{{ 'Global' | PageParameter:'SpouseGuid' | SanitizeSql }}'

Normalmente, si el valor debe ser de un tipo de datos específico, lo mejor es desinfectar primero el valor y, a continuación, dejar que SQL aplique el tipo. Esto garantiza que la entrada no válida se convierta correctamente o devuelva un resultado en blanco, lo que evita posibles ataques.

Así es como se ve si esperamos un valor GUID:

{% sql %}
DECLARE @SpouseGuid UNIQUEIDENTIFIER = '{{ 'Global' | PageParameter:'SpouseGuid' | SanitizeSql }}';
SELECT
  [NickName]
  , [LastName]
  , [Email]
FROM
  [Person]
WHERE
  [Guid] = @SpouseGuid;
{% endsql %}
{% for row in results %}
  <p>{{ row.NickName }} {{ row.LastName }}: {{ row.Email }}</p>
{% endfor %}

(UNIQUEIDENTIFIER es lo que SQL Server llama un GUID).

Si el valor esperado fuera un entero (número entero), podría utilizar DECLARE @myId INT = '[tu lava]' y eso asegurará que no haya caracteres no enteros como ' o -- que permita que el valor cambie la naturaleza de su consulta.


Conclusión:

La inyección SQL es un riesgo muy real cada vez que se utiliza algo externo como parte de una consulta. Pero ahora ya sabes cómo detectarlo, y tienes algunas herramientas para proteger tus consultas si Lava no es suficiente.

¡Ojalá esta escuela tuviera un desarrollador con tus conocimientos!

Escrito por Analista ministerial, formador en Triumph

A trabajar

¿Listo para dar vida a tus ideas de Rock RMS?

Estamos aquí para ayudar.

Contacto