Riesgos de seguridad

Las bases de datos contienen a menudo datos sensibles y permiten realizar operaciones peligrosas. Para trabajar con Nette Database de forma segura es crucial:

  • entender la diferencia entre las API seguras y las inseguras
  • usar 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 al trabajar con bases de datos. Se produce cuando una entrada de usuario sin sanear pasa a formar parte de una consulta SQL. Un atacante puede insertar sus propios comandos SQL y con ello:

  • obtener acceso no autorizado a los datos
  • modificar o borrar datos de la base de datos
  • saltarse 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 users WHERE name = '' OR '1'='1'
// Que devuelve todos los usuarios

Lo mismo vale para Database Explorer:

// ❌ CÓDIGO PELIGROSO - vulnerable a la inyección SQL
$table->where('name = ' . $_GET['name']);
$table->where("name = '$_GET[name]'");

Consultas parametrizadas

La defensa fundamental contra la inyección SQL son las consultas parametrizadas. Nette Database ofrece varias formas de usarlas.

La más sencilla es usar marcadores con signo de interrogación:

// ✅ Consulta parametrizada segura
$database->query('SELECT * FROM users WHERE name = ?', $name);

// ✅ Condición segura en Explorer
$table->where('name = ?', $name);

Esto vale para todos los demás métodos de Database Explorer que permiten insertar expresiones con marcadores de interrogación y parámetros.

Para los comandos INSERT y UPDATE o para 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 los valores de los parámetros

Las consultas parametrizadas son la piedra angular del trabajo seguro con la base de datos. Pero los valores que insertamos en ellas deben pasar por varios niveles de comprobación:

Comprobación del tipo

Lo más importante es asegurar el tipo de dato correcto de los parámetros; es una condición necesaria para usar Nette Database con seguridad. La base de datos da por hecho que todos los datos de entrada tienen el tipo de dato correcto, el que corresponde a cada columna.

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 provocaría un error. Por eso, nunca use datos sin validar de $_GET, $_POST o $_COOKIE directamente en las consultas a la base de datos.

Validación del formato

En el segundo nivel comprobamos 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 esa columna.

Para este nivel de validación podemos apoyarnos en parte en la propia base de datos: muchas rechazan los datos no válidos. Pero el comportamiento puede variar; algunas pueden truncar en silencio las cadenas largas o recortar los números que se salen del rango.

Validación específica del dominio

El tercer nivel son las comprobaciones lógicas específicas de su aplicación. Por ejemplo, verificar que los valores de los desplegables corresponden a las opciones ofrecidas, que los números están dentro del rango esperado (p. ej. una edad de 0 a 150 años) o que las dependencias mutuas entre valores tienen sentido.

Métodos de validación recomendados

  • Use Nette Forms, que aseguran automáticamente la validación correcta de todas las entradas.
  • Use presenters e indique los tipos de dato de los parámetros en los métodos action*() y render*().
  • O implemente su propia capa de validación con las herramientas estándar de PHP, como filter_var().

Trabajo seguro con las columnas

En la sección anterior hemos mostrado cómo validar correctamente los valores de los parámetros. Pero, al usar arrays en las consultas SQL, hay que prestar la misma atención a sus claves.

// ❌ CÓDIGO PELIGROSO - las claves del array no están saneadas
$database->query('INSERT INTO users', $_POST);

En los comandos INSERT y UPDATE esto es un fallo de seguridad crítico: un atacante puede insertar o modificar cualquier columna de la base de datos. Podría, por ejemplo, poner is_admin = 1 o insertar datos arbitrarios en columnas sensibles (la llamada Mass Assignment Vulnerability).

En las condiciones WHERE es aún más peligroso, porque pueden contener operadores:

// ❌ CÓDIGO PELIGROSO - las claves del array no están saneadas
$_POST['salary >'] = 100000;
$database->query('SELECT * FROM users WHERE', $_POST);
// ejecuta la consulta WHERE (`salary` > 100000)

Un atacante puede usar este enfoque para descubrir sistemáticamente los salarios de los empleados. Podría empezar, por ejemplo, con una consulta de los salarios superiores a 100 000, luego los inferiores a 50 000 y, estrechando el rango poco a poco, revelar los salarios aproximados de todos los empleados. A este tipo de ataque se le llama enumeración SQL.

Los métodos where() y whereOr() son incluso mucho más flexibles y admiten expresiones SQL, incluidos operadores y funciones, tanto en las claves como en los valores. Eso 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 termina la condición original con 0), añade su propio SELECT mediante UNION para obtener datos sensibles de la tabla users y cierra la consulta de forma sintácticamente correcta con WHERE (1).

Lista blanca de columnas

Para trabajar con seguridad con los nombres de las columnas necesitamos un mecanismo que asegure que el usuario solo puede trabajar con las columnas permitidas y no puede añadir las suyas. Podríamos intentar detectar y bloquear los nombres de columna peligrosos (lista negra), pero ese enfoque no es fiable: un atacante siempre puede idear una nueva forma de escribir un nombre de columna peligroso que no habíamos previsto.

Por eso 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 modificar
$allowedColumns = ['name', 'email', 'active'];

// Elimina de la entrada todas las columnas no autorizadas
$filteredData = array_intersect_key($userData, array_flip($allowedColumns));

// ✅ Ahora es seguro usarlo 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 use el marcador ?name. Este asegura el escapado correcto de los identificadores según la sintaxis de cada base de datos (p. ej. con acentos graves en MySQL):

// ✅ Uso seguro de identificadores de confianza
$table = 'users';
$column = 'name';
$database->query('SELECT ?name FROM ?name', $column, $table);
// Resultado en MySQL: SELECT `name` FROM `users`

Importante: use el símbolo ?name solo para valores de confianza definidos en el código de la aplicación. Para los valores que vienen del usuario, use de nuevo una lista blanca. De lo contrario se expone a riesgos de seguridad:

// ❌ PELIGROSO - nunca use la entrada del usuario
$database->query('SELECT ?name FROM users', $_GET['column']);
versión: 4.x