Cualquier sentencia SQL construida uniendo cadenas con valores no confiables puede ser objeto de inyección, y la solución es la misma en todos los lenguajes: mantener el SQL fijo y enlazar los valores como parámetros. La OWASP Query Parameterization Cheat Sheet clasifica la inyección SQL en A05:2025-Injection del OWASP Top 10:2025 y la define como una de las vulnerabilidades web más peligrosas.
Por qué falla la concatenación
SQL no distingue realmente entre el plano de control y el plano de datos. Un atacante introduce un metacarácter en una entrada y aparecen comandos SQL donde antes no había ninguno. La página de la comunidad OWASP sobre inyección SQL muestra una consulta en C# construida por concatenación:
string query = "SELECT * FROM items WHERE owner = '"
+ userName + "' AND itemname = '"
+ ItemName.Text + "'";
Si el atacante introduce name' OR 'a'='a como nombre del artículo, la condición se evalúa siempre como verdadera. La consulta devuelve todas las filas de la tabla items, sea cual sea su propietario. Según las funciones y los privilegios de la base de datos, el mismo fallo también puede modificar datos, afectar a archivos o ejecutar comandos del sistema operativo.
El origen del valor no importa. Los controles se aplican tanto si los datos proceden de una petición HTTP, de otro servicio o de datos almacenados previamente.
Localice las consultas construidas con cadenas
Empiece con un grep de palabras clave SQL cercanas a una concatenación o a un formateo. Estos patrones son una primera aproximación, no un escáner. Generan mucho ruido y no detectan las consultas repartidas en varias líneas.
# concatenation (Java, C#, JavaScript, PHP uses "." instead of "+")
grep -rnEi "(select|insert into|update|delete from)[^;]*[\"'] *\+" src/
# Python f-strings and .format()
grep -rnEi "f[\"'](select|insert|update|delete)|(select|insert|update|delete).*\.format\(" .
# JavaScript template literals
grep -rnEi '`(select|insert|update|delete)[^`]*\$\{' src/
# sprintf-style formatting
grep -rnEi "sprintf\(.*(select|insert|update|delete)" .
Revise cada resultado y compruebe que los valores no confiables se enlazan en lugar de concatenarse. Después, mire en los sitios que un grep suele pasar por alto:
- Cadenas de consulta de ORM. Las consultas de Hibernate pueden ser vulnerables cuando los valores se concatenan en la cadena de consulta en lugar de enlazarse.
- Procedimientos almacenados. Mantienen el SQL en la base de datos, pero no son seguros por defecto. OWASP muestra un procedimiento PL/SQL que une
usreitmen una cadena de consulta y afirma que es «vulnerable al mismo ataque de inyección SQL mostrado en el primer ejemplo». Enlazar los argumentos de la llamada protege la llamada, no el SQL dinámico construido dentro del procedimiento. - Parametrización en el cliente. Muchas bibliotecas del lado del cliente construyen las consultas por concatenación de cadenas y envían la consulta en bruto al servidor. Confirme que la parametrización se realiza en el servidor.
- Identificadores dinámicos y sentidos de ordenación. No pueden enlazarse, así que son el lugar donde la concatenación sobrevive a una revisión de parametrización.
Después, pruebe. La OWASP Web Security Testing Guide incluye pruebas de inyección SQL para pruebas manuales y automatizadas, con técnicas ciegas y fuera de banda. Para una comprobación externa de lo que su revisión haya pasado por alto, ese es el cometido de una prueba de penetración.
Sustitúyalas por parámetros
Defina la estructura SQL por separado de sus valores de datos y luego enlace los valores a través de la API de la base de datos. Este es el ejemplo de la OWASP SQL Injection Prevention Cheat Sheet, sin la limpieza ni la gestión de errores:
String query = "SELECT account_balance FROM user_data WHERE user_name = ?";
PreparedStatement statement = connection.prepareStatement(query);
statement.setString(1, customerName);
ResultSet results = statement.executeQuery();
Un valor como tom' or '1'='1 se trata ahora como el nombre de usuario literal que debe coincidir. Concatenar un valor no confiable en la consulta antes de prepararla anula la protección.
Con un ORM, enlace también dentro de la cadena de consulta. Los ejemplos de OWASP usan un parámetro con nombre en HQL, FROM Inventory i WHERE i.productDescription = :productDescription, y pasan el valor con setParameter.
Para el SQL dinámico dentro de procedimientos almacenados, utilice variables de enlace. En Oracle PL/SQL eso significa EXECUTE IMMEDIATE con USING. En SQL Server, sp_executesql con una lista de parámetros declarada. Los procedimientos sin SQL dinámico no necesitan nada especial, porque sus parámetros ya están enlazados.
El enlace impide que un valor modifique la sintaxis SQL. No decide si quien llama puede acceder a la cuenta correspondiente, así que sus comprobaciones de autorización deben mantenerse.
Lo que los parámetros no pueden cubrir
Los marcadores de parámetro representan valores de datos. No pueden representar nombres de tablas, nombres de columnas ni palabras clave como ASC y DESC. Cuando los usuarios deban elegir, asocie la selección a un pequeño conjunto de tokens propios de la aplicación y rechace cualquier valor desconocido. Antes:
String query = "SELECT id, name FROM items ORDER BY name " + sortParam;
Después:
Map<String, String> directions = Map.of("asc", "ASC", "desc", "DESC");
String direction = directions.get(sortParam);
if (direction == null) {
throw new IllegalArgumentException("Unknown sort direction");
}
String query = "SELECT id, name FROM items ORDER BY name " + direction;
El texto que se añade a la consulta es ahora siempre uno de dos literales, nunca lo que envió el usuario. Siga enlazando todos los valores de datos de la misma consulta, aunque superen comprobaciones de tipo, longitud o formato.
No recurra al escapado manual. Depende del dialecto y de la configuración de SQL: el modo NO_BACKSLASH_ESCAPES de MySQL, por ejemplo, desactiva el escapado con barra invertida. OWASP califica el escapado como muy desaconsejable y describe las listas de denegación como plagadas de resquicios.
Limite el daño
El mínimo privilegio no previene la inyección SQL. Limita lo que un atacante puede hacer a través de una cuenta de base de datos, así que aplíquelo junto con los parámetros.
- Parta de cero y añada los derechos que necesita cada cuenta de aplicación. Averiguar qué quitar de una concesión amplia es la forma en que se pasan derechos por alto.
- Dar acceso de DBA o administrador a una cuenta de aplicación es el atajo habitual, porque todo «simplemente funciona». OWASP lo califica de muy peligroso.
- Una página de inicio de sesión necesita acceso de lectura a los campos de nombre de usuario y contraseña, y ninguno de escritura. Una página de registro necesita insertar. Esa separación solo es aplicable si ambas usan usuarios de base de datos distintos, así que dé a cada aplicación web el suyo propio.
- Los atacantes no necesitan una inyección para abusar de una cuenta con demasiados privilegios. Pueden cambiar un parámetro de un valor legal a uno para el que no están autorizados pero la aplicación sí, por lo que las concesiones acotadas ayudan incluso donde las consultas son correctas.
- Una vista puede exponer solo los campos que necesita una cuenta, concediendo
SELECTsobre la vista y no sobre la tabla. Compruebe antes las reglas de su base de datos: PostgreSQL normalmente comprueba el acceso a las tablas subyacentes con los privilegios del propietario de la vista. Las consultas contra vistas siguen necesitando parámetros. - La cuenta del sistema operativo también importa. No ejecute el DBMS como root o system. OWASP señala que MySQL se ejecuta como system en Windows por defecto.
Por dónde empezar
- Enumere todos los lugares que construyen SQL: código de aplicación, cadenas de consulta de ORM y procedimientos almacenados que usan SQL dinámico. Ejecute los patrones grep anteriores como primera pasada.
- Sustituya los valores concatenados por parámetros enlazados, en el servidor, y reconstruya el SQL dinámico dentro de los procedimientos con variables de enlace.
- Sustituya las elecciones dinámicas de tabla, columna y ordenación por asignaciones fijas que rechacen las entradas desconocidas.
- Audite las cuentas de base de datos. Retire los derechos de DBA y administrador de las cuentas de aplicación, dé a cada aplicación su propio usuario y traslade el acceso de solo lectura a concesiones acotadas o a vistas.
- Ejecute sobre el resultado las pruebas de inyección SQL de la Web Security Testing Guide, o pida a nuestro equipo de desarrollo de software seguro que revise los cambios.
