Pentest

Inyección SQL: cómo ocurre y cómo evitarla

Es una de las fallas más viejas de la web y sigue apareciendo, casi siempre por la misma razón: alguien armó una consulta pegando pedazos de texto, y uno de esos pedazos venía de un formulario. Entender el mecanismo lleva cinco minutos, y la corrección, un poco menos.

Cómo ocurre

La base de datos recibe una cadena de texto con una instrucción y la interpreta. Si esa cadena se construye uniendo texto fijo con datos del usuario, el motor no tiene forma de saber dónde termina lo que escribió el programador y dónde empieza lo que escribió otra persona. Todo es una sola instrucción.

Este código es vulnerable:

# VULNERABLE: no usar
def buscar_usuario(conn, email):
    consulta = "SELECT id, nombre FROM usuarios WHERE email = '" + email + "'"
    return conn.execute(consulta).fetchall()

Con un email normal, todo anda. El ejemplo de manual es qué pasa si el valor recibido contiene una comilla y un fragmento más:

' OR '1'='1

La consulta resultante queda con una condición que siempre es verdadera y devuelve todos los usuarios. En el peor caso, según los permisos de la cuenta y el motor, un atacante podría leer otras tablas, modificar datos o incluso afectar el servidor. Lo importante para quien desarrolla es la idea: la entrada dejó de ser un dato y pasó a ser parte de la estructura de la consulta.

La corrección: consultas parametrizadas

La solución es mandar la instrucción y los valores por separado. El motor compila la consulta con marcadores de posición y recibe los datos después, ya como valores, sin posibilidad de reinterpretarlos.

def buscar_usuario(conn, email):
    consulta = "SELECT id, nombre FROM usuarios WHERE email = ?"
    return conn.execute(consulta, (email,)).fetchall()

El marcador (? en este caso, otros drivers usan %s o nombres) depende de la biblioteca, pero el principio es el mismo. Con esta forma, la entrada anterior se busca literalmente como un email raro y no encuentra nada.

Lo que no sirve como defensa principal es intentar escapar comillas a mano o filtrar palabras prohibidas. Son parches frágiles: dependen del motor, de la codificación y de que nadie se olvide de aplicarlos en un solo lugar.

ORMs y sus excepciones

Un ORM bien usado parametriza por vos, y por eso reduce mucho el riesgo. Pero casi todos tienen salidas hacia SQL crudo: métodos para ejecutar texto directo, fragmentos que se concatenan en un filtro, ordenamientos armados con el nombre de una columna que viene del cliente. Ahí la protección desaparece.

Revisá especialmente:

Buscar esos puntos con una búsqueda de texto en el repositorio es una tarea de media hora que rinde mucho.

Reducir el daño posible

Imaginá que alguna falla se te escapó. La cuenta con la que la aplicación se conecta a la base define cuánto se puede hacer. Si es la cuenta con todos los privilegios, un error de código se convierte en un desastre. Aplicá privilegios mínimos:

  1. Una cuenta distinta para la aplicación, sin permisos de administración del motor.
  2. Solo los permisos que necesita sobre las tablas que usa (leer, insertar, actualizar según corresponda).
  3. Cuentas separadas para tareas como migraciones, que no corran dentro de la aplicación.

Validación y mensajes de error

Validar entradas ayuda como defensa complementaria. Si un campo debe ser un número, verificá que lo sea; si es un código de seis letras, rechazá lo que no coincida. Reduce la superficie, pero no reemplaza a la parametrización, porque hay campos de texto libre legítimos (un apellido con apóstrofe, por ejemplo) que no se pueden filtrar sin romper a usuarios reales.

Cuidá también los mensajes de error. Una página que muestra el texto de la consulta fallida o el mensaje del motor le da a un atacante un mapa de tu esquema. En producción, devolvé un mensaje genérico al usuario y dejá el detalle en los registros del servidor, donde lo ves vos.

Cómo detectarla

Revisá el código buscando concatenaciones en consultas, usá análisis estático que marque esos patrones y probá tus propios formularios en un ambiente de prueba con entradas con comillas y caracteres especiales. Si algo se rompe o cambia de comportamiento, ya tenés el punto de partida. Una buena costumbre es agregar una prueba automática por cada consulta que reciba datos externos, así el error no vuelve en un cambio futuro.