Riesgos de seguridad
Las bases de datos contienen a menudo datos sensibles y permiten realizar operaciones peligrosas. Para trabajar de forma segura con Nette Database, los aspectos clave son:
- Entender la diferencia entre API segura e insegura
- Utilizar consultas parametrizadas
- Validar correctamente los datos de entrada
¿Qué es la inyección SQL?
La inyección SQL es el riesgo de seguridad más grave cuando se trabaja con bases de datos. Se produce cuando una entrada de usuario no filtrada pasa a formar parte de una consulta SQL. Un atacante puede insertar sus propios comandos SQL y de este modo
- Extraer datos no autorizados
- Modificar o eliminar datos de la base de datos
- eludir la autenticación
// ❌ CÓDIGO PELIGROSO - vulnerable a la inyección SQL.
$database->query("SELECT * FROM users WHERE name = '$_GET[name]'");
// Un atacante podría introducir un valor como: ' OR '1'='1
// La consulta resultante sería: SELECT * FROM usuarios WHERE nombre = '' OR '1'='1'
// Que devuelve todos los usuarios
Lo mismo se aplica a Database Explorer:
// ❌ CÓDIGO PELIGROSO - vulnerable a la inyección SQL.
$table->where('name = ' . $_GET['name']);
$table->where("name = '$_GET[name]'");
Consultas parametrizadas seguras
La defensa fundamental contra la inyección SQL son las consultas parametrizadas. Nette Database ofrece varias formas de utilizarlas.
La forma más sencilla es utilizar marcadores de interrogación:
// ✅ Consulta parametrizada segura
$database->query('SELECT * FROM users WHERE name = ?', $name);
// ✅ Condición segura en el Explorador
$table->where('name = ?', $name);
Esto se aplica a todos los demás métodos de Database Explorer que permiten insertar expresiones con marcadores de interrogación y parámetros.
Para las cláusulas INSERT
, UPDATE
, o WHERE
, puede pasar valores en una matriz:
// ✅ Inserción segura
$database->query('INSERT INTO users', [
'name' => $name,
'email' => $email,
]);
// ✅ Inserción segura en Explorer
$table->insert([
'name' => $name,
'email' => $email,
]);
Validación de valores de parámetros
Las consultas parametrizadas son la piedra angular del trabajo seguro con bases de datos. Sin embargo, los valores que se les pasan deben someterse a varios niveles de validación:
Comprobación de tipo
Asegurar el tipo de datos correcto de los parámetros es crítico-es una condición necesaria para el uso seguro de Nette Database. La base de datos asume que todos los datos de entrada tienen el tipo de datos correcto correspondiente a la columna.
Por ejemplo, si $name
en los ejemplos anteriores se convirtiera inesperadamente en una matriz en lugar de una
cadena, Nette Database intentaría insertar todos sus elementos en la consulta SQL, lo que provocaría un error. Por lo tanto,
nunca utilice datos no validados de $_GET
, $_POST
, o $_COOKIE
directamente en
consultas a la base de datos.
Validación de formatos
El segundo nivel comprueba el formato de los datos, por ejemplo, asegurándose de que las cadenas están codificadas en UTF-8 y su longitud coincide con la definición de la columna, o verificando que los valores numéricos se encuentran dentro del rango permitido para el tipo de datos de la columna.
A este nivel, puede confiar parcialmente en la propia base de datos: muchas bases de datos rechazan los datos no válidos. Sin embargo, el comportamiento puede variar: algunas pueden truncar cadenas largas silenciosamente, o recortar números que estén fuera de rango.
Validación específica de dominio
El tercer nivel implica comprobaciones lógicas específicas de su aplicación. Por ejemplo, verificar que los valores de los cuadros de selección coincidan con las opciones disponibles, que los números se encuentren dentro de un rango esperado (por ejemplo, edad 0–150 años) o que las relaciones entre valores tengan sentido.
Métodos de validación recomendados
- Utilice Nette Forms, que gestiona automáticamente la validación adecuada de todas las entradas.
- Utilice Presentadores y declare los tipos de datos de los parámetros en
los métodos
action*()
yrender*()
. - O implemente una capa de validación personalizada usando herramientas PHP estándar como
filter_var()
.
Trabajo Seguro con Columnas
En la sección anterior, cubrimos cómo validar correctamente los valores de los parámetros. Sin embargo, cuando se usan arrays en consultas SQL, se debe prestar la misma atención a sus claves.
// ❌ CÓDIGO PELIGROSO - las claves de array no están desinfectadas.
$database->query('INSERT INTO users', $_POST);
En el caso de los comandos INSERT y UPDATE, se trata de un fallo de seguridad importante: un atacante puede insertar
o modificar cualquier columna de la base de datos. Podrían, por ejemplo, establecer is_admin = 1
o insertar datos
arbitrarios en columnas sensibles (conocido como Vulnerabilidad de Asignación Masiva).
En las condiciones WHERE, es aún más peligroso porque pueden contener operadores:
// CÓDIGO PELIGROSO - las claves de los arrays no están desinfectadas
$_POST['salary >'] = 100000;
$database->query('SELECT * FROM users WHERE', $_POST);
// ejecuta la consulta WHERE (`salario` > 100000)
Un atacante puede utilizar este enfoque para descubrir sistemáticamente los salarios de los empleados. Puede empezar con una consulta de salarios superiores a 100.000, luego inferiores a 50.000 y, reduciendo gradualmente el rango, puede revelar los salarios aproximados de todos los empleados. Este tipo de ataque se denomina enumeración SQL.
Los métodos where()
y whereOr()
son aún más flexibles y admiten expresiones SQL, incluidos operadores y
funciones, tanto en las claves como en los valores. Esto ofrece a un atacante la posibilidad de realizar inyecciones SQL
complejas:
// ❌ CÓDIGO PELIGROSO - el atacante puede insertar su propio SQL
$_POST = ['0) UNION SELECT name, salary FROM users WHERE (1'];
$table->where($_POST);
// ejecuta la consulta WHERE (0) UNION SELECT nombre, salario FROM usuarios WHERE (1)
Este ataque termina la condición original con 0)
, añade su propio SELECT
usando UNION
para obtener datos sensibles de la tabla users
, y cierra con una consulta sintácticamente correcta usando
WHERE (1)
.
Lista blanca de columnas
Para trabajar de forma segura con los nombres de columna, necesitas un mecanismo que garantice que los usuarios sólo puedan interactuar con las columnas permitidas y no puedan añadir las suyas propias. Intentar detectar y bloquear nombres de columna peligrosos (listas negras) no es fiable: un atacante siempre puede inventar una nueva forma de escribir un nombre de columna peligroso que no hayas previsto.
Por lo tanto, es mucho más seguro invertir la lógica y definir una lista explícita de columnas permitidas (listas blancas):
// Columnas que el usuario está autorizado a modificar
$allowedColumns = ['name', 'email', 'active'];
// Eliminar todas las columnas no autorizadas de la entrada
$filteredData = array_intersect_key($userData, array_flip($allowedColumns));
// ✅ Ahora se puede utilizar con seguridad en consultas, como:
$database->query('INSERT INTO users', $filteredData);
$table->update($filteredData);
$table->where($filteredData);
Identificadores dinámicos
Para los nombres dinámicos de tablas y columnas, utilice el marcador de posición ?name
. De este modo, se
garantiza que los identificadores se escapan correctamente de acuerdo con la sintaxis de la base de datos (por ejemplo, utilizando
puntos suspensivos en MySQL):
// ✅ Uso seguro de identificadores de confianza
$table = 'users';
$column = 'name';
$database->query('SELECT ?name FROM ?name', $column, $table);
// Resultado en MySQL: SELECT `nombre` FROM `usuarios`
Importante: Utilice el símbolo ?name
sólo para los valores de confianza definidos en el código de la
aplicación. Para los valores proporcionados por el usuario, vuelva a utilizar una lista
blanca. De lo contrario, corres el riesgo de sufrir vulnerabilidades de seguridad:
// ❌ PELIGROSO - no utilizar nunca la entrada del usuario
$database->query('SELECT ?name FROM users', $_GET['column']);