Riesgos de seguridad
La base de datos a menudo contiene datos sensibles y permite realizar operaciones peligrosas. Para trabajar de forma segura con Nette Database es clave:
- Comprender la diferencia entre API segura y peligrosa
- Usar consultas parametrizadas
- Validar correctamente los datos de entrada
¿Qué es SQL Injection?
SQL injection es el riesgo de seguridad más grave al trabajar con bases de datos. Ocurre cuando la entrada no tratada del usuario se convierte en parte de una consulta SQL. Un atacante puede insertar sus propios comandos SQL y así:
- Obtener acceso no autorizado a los datos
- Modificar o eliminar datos en la base de datos
- Omitir la autenticación
// ❌ CÓDIGO PELIGROSO - vulnerable a inyección SQL
$database->query("SELECT * FROM users WHERE name = '$_GET[name]'");
// Un atacante puede introducir, por ejemplo, el valor: ' OR '1'='1
// La consulta resultante será: SELECT * FROM users WHERE name = '' OR '1'='1'
// Lo que devolverá todos los usuarios
Lo mismo se aplica a Database Explorer:
// ❌ CÓDIGO PELIGROSO - vulnerable a inyección SQL
$table->where('name = ' . $_GET['name']);
$table->where("name = '$_GET[name]'");
Consultas parametrizadas
La defensa básica contra la inyección SQL son las consultas parametrizadas. Nette Database ofrece varias formas de usarlas.
La forma más sencilla es usar signos de interrogación como marcadores de posición:
// ✅ Consulta parametrizada segura
$database->query('SELECT * FROM users WHERE name = ?', $name);
// ✅ Condición segura en Explorer
$table->where('name = ?', $name);
Esto se aplica a todos los demás métodos en Database Explorer que permiten insertar expresiones con marcadores de posición y parámetros.
Para los comandos INSERT
, UPDATE
o la cláusula WHERE
, podemos pasar los valores en
un array:
// ✅ INSERT seguro
$database->query('INSERT INTO users', [
'name' => $name,
'email' => $email,
]);
// ✅ INSERT seguro 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 insertamos en ellas deben pasar por varios niveles de control:
Control de tipo
Lo más importante es asegurar el tipo de dato correcto de los parámetros – esta 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 dato correcto correspondiente a la columna dada.
Por ejemplo, si $name
en los ejemplos anteriores fuera inesperadamente un array en lugar de una cadena, Nette
Database intentaría insertar todos sus elementos en la consulta SQL, lo que llevaría a un error. Por lo tanto, nunca uses
datos no validados de $_GET
, $_POST
o $_COOKIE
directamente en las consultas de base
de datos.
Control de formato
En el segundo nivel, verificamos el formato de los datos, por ejemplo, si las cadenas están en codificación UTF-8 y su longitud corresponde a la definición de la columna, o si los valores numéricos están dentro del rango permitido para el tipo de dato de la columna.
En este nivel de validación, podemos confiar parcialmente en la propia base de datos: muchas bases de datos rechazarán datos no válidos. Sin embargo, el comportamiento puede variar, algunas pueden truncar silenciosamente cadenas largas o recortar números fuera de rango.
Control de dominio
El tercer nivel son los controles lógicos específicos de tu aplicación. Por ejemplo, verificar que los valores de los select boxes correspondan a las opciones ofrecidas, que los números estén en el rango esperado (por ejemplo, edad 0–150 años) o que las dependencias mutuas entre los valores tengan sentido.
Métodos de validación recomendados
- Usa Nette Forms, que aseguran automáticamente la validación correcta de todas las entradas.
- Usa Presenters e indica los tipos de datos para los parámetros en los
métodos
action*()
yrender*()
. - O implementa tu propia capa de validación usando herramientas estándar de PHP como
filter_var()
.
Trabajo seguro con columnas
En la sección anterior, mostramos cómo validar correctamente los valores de los parámetros. Sin embargo, al usar arrays en consultas SQL, debemos prestar la misma atención a sus claves.
// ❌ CÓDIGO PELIGROSO - las claves en el array no están tratadas
$database->query('INSERT INTO users', $_POST);
Para los comandos INSERT
y UPDATE
, este es un error de seguridad fundamental: un atacante puede
insertar o cambiar cualquier columna en la base de datos. Podría, por ejemplo, establecer is_admin = 1
o insertar
datos arbitrarios en columnas sensibles (la llamada Vulnerabilidad de Asignación Masiva).
En las condiciones WHERE
, es aún más peligroso, ya que pueden contener operadores:
// ❌ CÓDIGO PELIGROSO - las claves en el array no están tratadas
$_POST['salary >'] = 100000;
$database->query('SELECT * FROM users WHERE', $_POST);
// ejecuta la consulta WHERE (`salary` > 100000)
Un atacante puede usar este enfoque para averiguar sistemáticamente los salarios de los empleados. Comenzará, por ejemplo, con una consulta sobre salarios superiores a 100.000, luego inferiores a 50.000, y reduciendo gradualmente el rango, puede descubrir los salarios aproximados de todos los empleados. Este tipo de ataque se llama enumeración SQL.
Los métodos where()
y whereOr()
son aún mucho más flexibles y admiten expresiones SQL, incluidos
operadores y funciones, en claves y valores. Esto le da al atacante la posibilidad de realizar una inyección SQL:
// ❌ 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 name, salary FROM users WHERE (1)
Este ataque finaliza la condición original con 0)
, adjunta su propio SELECT
usando
UNION
para obtener datos sensibles de la tabla users
y cierra la consulta sintácticamente correcta con
WHERE (1)
.
Lista blanca de columnas
Para trabajar de forma segura con los nombres de las columnas, necesitamos un mecanismo que garantice que el usuario solo pueda trabajar con las columnas permitidas y no pueda agregar las suyas propias. Podríamos intentar detectar y bloquear nombres de columnas peligrosos (lista negra), pero este enfoque no es fiable: un atacante siempre puede encontrar una nueva forma de escribir un nombre de columna peligroso que no previmos.
Por lo tanto, es mucho más seguro invertir la lógica y definir una lista explícita de columnas permitidas (lista blanca):
// Columnas que el usuario puede editar
$allowedColumns = ['name', 'email', 'active'];
// Eliminamos todas las columnas no permitidas de la entrada
$filteredData = array_intersect_key($userData, array_flip($allowedColumns)); // Use array_flip for keys
// ✅ Ahora podemos usar $filteredData de forma segura en consultas, como por ejemplo:
$database->query('INSERT INTO users', $filteredData);
$table->update($filteredData);
$table->where($filteredData);
Identificadores dinámicos
Para nombres dinámicos de tablas y columnas, usa el marcador de posición ?name
. Esto asegura el escape correcto
de los identificadores según la sintaxis de la base de datos dada (por ejemplo, usando comillas invertidas en MySQL):
// ✅ Uso seguro de identificadores confiables definidos en la aplicación
$table = 'users';
$column = 'name';
$database->query('SELECT ?name FROM ?name', $column, $table);
// Resultado en MySQL: SELECT `name` FROM `users`
Importante: usa el símbolo ?name
solo para valores confiables definidos en el código de la aplicación. Para
valores del usuario, usa nuevamente la lista blanca. De lo contrario, te expones a riesgos de
seguridad:
// ❌ PELIGROSO - nunca uses la entrada del usuario para nombres de columnas/tablas
$database->query('SELECT ?name FROM users', $_GET['column']);