Los errores detrás de las vulnerabilidades - Parte 5
Bienvenido a la última entrega de nuestra serie de cinco partes que analiza los errores de código responsables de las vulnerabilidades y exploits de los que intentamos mantenernos a salvo. En esta parte, repasaremos las últimas cinco entradas del Mitre CWE Top 25del nº 5 al nº 1.
Puede encontrar parte 1 aquí, la parte 2 aquí, parte 3 aquíy la parte 4 aquí.
5. Lectura fuera de límites
La lectura fuera de los límites se produce cuando un producto lee datos más allá del final o antes del principio del búfer previsto. Esto puede permitir a los atacantes leer información confidencial de otras ubicaciones de memoria o provocar un bloqueo.
Por ejemplo, considere el siguiente fragmento de código C que lee de un array utilizando una entrada controlada por el usuario:
#include <stdio.h> int get_element(int *array, int len, int index) { int result; if (index < len) { result = array[index]; } else { printf("Invalid index: %d\n", index); result = -1; } return result; } int main() { int my_array[5] = {10, 20, 30, 40, 50}; int index = -1; // Simulating user-controlled input int value = get_element(my_array, 5, index); printf("Value at index %d: %d\n", index, value); return 0; }
En este ejemplo, la función get_element comprueba si el índice proporcionado está dentro del límite máximo de la matriz, pero no comprueba el límite mínimo. Esto permite que valores negativos sean aceptados como índices válidos de la matriz, resultando en una lectura fuera de los límites. Para mitigar este problema, añada una comprobación del límite mínimo:
if (index >= 0 && index < len) { result = array[index]; }
4. Validación incorrecta de las entradas
La validación incorrecta de la entrada se produce cuando un producto recibe una entrada o datos pero no los valida o los valida incorrectamente, lo que provoca una alteración del flujo de control, el control arbitrario de un recurso o la ejecución arbitraria de código.
private void buildList(int untrustedListSize) { if (0 > untrustedListSize) { die("Negative value supplied for list size, die evil hacker!"); } Widget[] list = new Widget[untrustedListSize]; list[0] = new Widget(); }
En este ejemplo, el código comprueba si untrustedListSize es negativo, pero no comprueba si es cero. Si se proporciona un valor cero, el código construirá un array de tamaño 0 e intentará almacenar un nuevo widget en la primera ubicación, provocando una excepción. Para evitar este problema, utilice técnicas de validación de entrada exhaustivas, teniendo en cuenta todas las propiedades potencialmente relevantes de la entrada, y no se base exclusivamente en la búsqueda de entradas maliciosas o malformadas. En este caso, deberíamos comprobar si untrustedListSize es mayor que 0.
3. Neutralización incorrecta de elementos especiales utilizados en un comando SQL (la temida "inyección SQL").
La inyección SQL se produce cuando una aplicación construye la totalidad o parte de un comando SQL utilizando entradas influenciadas externamente desde un componente ascendente sin neutralizar o neutralizando incorrectamente elementos especiales que podrían modificar el comando SQL pretendido.
#include <stdio.h> #include <stdlib.h> #include <string.h> int main(int argc, char *argv[]) { char query[1024]; char *user_input = argv[1]; snprintf(query, sizeof(query), "SELECT * FROM users WHERE username='%s'", user_input); printf("Generated query: %s\n", query); // Execute the query... return 0; }
En este ejemplo, la entrada del usuario se concatena directamente con la consulta SQL, lo que permite a un atacante inyectar código SQL malicioso. Para evitar la inyección SQL, utilice consultas parametrizadas, sentencias preparadas o procedimientos almacenados para separar la entrada del usuario de las sentencias SQL. En este caso, deberíamos utilizar sentencias preparadas:
#include <stdio.h> #include <stdlib.h> #include <mysql/mysql.h> (...) MYSQL_STMT *stmt = mysql_stmt_init(con); if (!stmt) { fprintf(stderr, "mysql_stmt_init() failed\n"); exit(1); } const char *query = "SELECT * FROM users WHERE username=?"; if (mysql_stmt_prepare(stmt, query, strlen(query)) != 0) { fprintf(stderr, "mysql_stmt_prepare() failed: %s\n", mysql_stmt_error(stmt)); exit(1); }
Al utilizar sentencias preparadas, la entrada del usuario se trata como datos y no como parte de la consulta SQL, lo que evita eficazmente la inyección SQL.
2. Neutralización inadecuada de entradas durante la generación de páginas web (o "Cross-Site Scripting")
Las vulnerabilidades de secuencias de comandos en sitios cruzados (XSS) se producen cuando una aplicación no neutraliza o neutraliza incorrectamente la entrada controlable por el usuario antes de colocarla en la salida utilizada como página web servida a otros usuarios.
Considere el siguiente fragmento de código PHP:
$nombredeusuario = $_GET['nombredeusuario']; haga eco de "Bienvenido, " . $nombredeusuario . "!";
En este ejemplo, la información introducida por el usuario se incrusta directamente en la página web sin una limpieza adecuada, lo que permite al agresor inyectar código HTML o JavaScript malicioso. Para evitar el XSS, valide y sanee la entrada del usuario, utilice técnicas de codificación de salida seguras al mostrar la entrada del usuario en páginas web y emplee políticas de seguridad de contenidos para reducir el impacto de un ataque XSS. En este caso, debemos desinfectar la entrada del usuario antes de mostrarla:
$nombredeusuario = htmlspecialchars($_GET['nombredeusuario'], ENT_QUOTES, 'UTF-8'); haga eco de "Bienvenido, " . $nombredeusuario . "!";
1. Escritura fuera de límites
La escritura fuera de los límites se produce cuando un producto escribe datos más allá del final o antes del principio del búfer previsto, lo que puede provocar la corrupción de datos, un bloqueo o la ejecución de código.
Por ejemplo, considere el siguiente fragmento de código C que escribe en una matriz:
#include <stdio.h> void fill_array(int *array, int len) { for (int i = 0; i <= len; i++) { array[i] = i * 10; } } int main() { int my_array[5]; fill_array(my_array, 5); for (int i = 0; i < 5; i++) { printf("Element at index %d: %d\n", i, my_array[i]); } return 0; }
In this example, the function `fill_array` writes to the array `my_array` using the loop variable `i`. However, the loop condition is `i <= len`, which allows the loop to run one iteration beyond the intended bounds of the array, causing an out-of-bounds write.
To avoid this issue, ensure that you use proper boundary checks when writing to buffers. In this case, the loop condition should be `i < len`:
for (int i = 0; i < len; i++) { array[i] = i * 10; }
Con esta modificación, el código escribe correctamente en la matriz dentro de sus límites, evitando una vulnerabilidad de escritura fuera de los límites.
Conclusión
Como conclusión, es crucial que los desarrolladores sean conscientes de los errores de código más comunes que pueden provocar vulnerabilidades en sus aplicaciones. Si comprenden estos problemas y aplican prácticas de codificación seguras, podrán reducir significativamente el riesgo de explotación por parte de los atacantes.
He aquí algunos principios generales de codificación segura que conviene tener en cuenta:
- Menor privilegio
Asegúrese de que sus aplicaciones se ejecutan con los privilegios mínimos necesarios para completar sus tareas. Esto limita el impacto potencial de una brecha de seguridad al reducir la capacidad del atacante para escalar sus privilegios u obtener acceso no autorizado a datos sensibles o recursos del sistema.
- Valores predeterminados seguros
Diseñe sus aplicaciones con la configuración de seguridad activada por defecto. Los usuarios deberían tener que desactivar explícitamente las funciones de seguridad en lugar de activarlas. Esto fomenta una experiencia de usuario más segura y reduce la probabilidad de configuraciones inseguras.
- Defensa en profundidad
Implemente varias capas de seguridad en sus aplicaciones, de modo que si se vulnera una capa, las demás puedan seguir proporcionando protección. Esto puede incluir el uso de validación de entrada, codificación de salida y controles de acceso en combinación para proporcionar una defensa integral contra diversos ataques.
- Fallar con seguridad
Diseñe sus aplicaciones para que fallen de forma segura en caso de error o entrada inesperada. Por ejemplo, si una aplicación encuentra un error al procesar la entrada de un usuario, no debe revelar información confidencial ni conceder acceso no deseado. En su lugar, debe gestionar el error con elegancia y proporcionar un mensaje de error útil para el usuario.
- Que sea sencillo
La complejidad es enemiga de la seguridad. Esfuérzate por simplificar tu código y minimizar el uso de construcciones complejas que pueden ser difíciles de entender y mantener. Esto facilita la detección de posibles problemas de seguridad y reduce la probabilidad de introducir nuevas vulnerabilidades durante el desarrollo o el mantenimiento.
- Actualice periódicamente las dependencias
Mantén actualizadas las dependencias de tu aplicación, ya que las librerías y frameworks obsoletos pueden introducir vulnerabilidades de seguridad. Comprueba periódicamente si hay actualizaciones y parches, e incorpóralos a tus procesos de desarrollo e implantación.
Al incorporar estos principios de codificación segura y abordar los errores de código comunes mencionados en esta serie, estará bien encaminado para desarrollar aplicaciones más seguras y proteger a sus usuarios de posibles amenazas a la seguridad.