Examen de problemas comunes de seguridad de código

Completado

Comprender las vulnerabilidades de seguridad comunes es esencial para identificar y resolver problemas de seguridad de código de forma eficaz. En esta unidad se tratan los problemas de seguridad frecuentes en el código, sus efectos y por qué abordarlos rápidamente es fundamental para la seguridad de las aplicaciones.

¿Por qué centrarse en los problemas de seguridad?

Las vulnerabilidades de seguridad representan una de las categorías más críticas de defectos de software. Una única vulnerabilidad puede dar lugar a:

  • Infracciones de datos: exposición de datos confidenciales al cliente o a los datos empresariales.
  • Pérdidas financieras: costos directos de infracciones, multas normativas y gastos de corrección.
  • Daño de reputación: pérdida de confianza de los clientes y credibilidad empresarial.
  • Interrupción operativa: los riesgos del sistema pueden detener las operaciones empresariales.

Los errores funcionales pueden resultar embarazosos para un desarrollador, pero los errores de seguridad pueden tener consecuencias graves para una organización y usuarios. Cada desarrollador debe ser consciente de la seguridad, independientemente de su rol o especialización.

Abrir proyecto de seguridad de aplicaciones web (OWASP)

Open Web Application Security Project (OWASP) es una organización sin ánimo de lucro centrada en mejorar la seguridad de software. OWASP mantiene el "OWASP Top 10" ampliamente reconocido: una lista actualizada periódicamente de los riesgos de seguridad de aplicaciones web más críticos en función de los datos de las organizaciones de seguridad de todo el mundo.

OWASP Top 10 sirve como línea base de seguridad para desarrolladores y organizaciones, lo que ayuda a priorizar qué vulnerabilidades abordar primero. Las clasificaciones cambian a lo largo del tiempo a medida que evolucionan los patrones de ataque. Por ejemplo:

  • 2017 OWASP Top 10: Los fallos de inyección ocuparon el puesto número 1.
  • 2021 OWASP Top 10: la inyección se movió a #3 a medida que surgen nuevas amenazas como el control de acceso roto.

El OWASP Top 10 refleja datos de ataque reales, no preocupaciones teóricas. Las vulnerabilidades de código, como la inyección SQL y el cifrado débil, se encuentran consistentemente entre los problemas de seguridad más críticos del sector.

Ataques por inyección

Los ataques de inyección se producen cuando los datos que no son de confianza se envían a un intérprete como parte de un comando o consulta. Los datos hostiles del atacante engañan al intérprete para ejecutar comandos no deseados o acceder a datos no autorizados.

Inyección de código SQL

La inyección de código SQL es uno de los ataques de inyección más peligrosos y comunes. Se produce cuando una aplicación incorpora una entrada que no es de confianza directamente en consultas SQL sin una validación o parametrización adecuadas.

Tenga en cuenta el siguiente código de ejemplo:

// DANGEROUS: Concatenating user input directly into SQL
string query = "SELECT * FROM Users WHERE Username = '" + userInput + "' AND Password = '" + passwordInput + "'";
SqlCommand command = new SqlCommand(query, connection);
SqlDataReader reader = command.ExecuteReader();

Un atacante podría escribir ' OR '1'='1 como entrada de nombre de usuario y transformar la consulta en:

SELECT * FROM Users WHERE Username = '' OR '1'='1' AND Password = ''

Dado que '1'='1' siempre es verdadero, esta consulta devuelve todos los usuarios, omitiendo por completo la autenticación.

Efectos reales

Los ataques por inyección de código SQL han provocado numerosas infracciones de alto perfil. Los atacantes pueden:

  • Omitir mecanismos de autenticación.
  • Extraiga bases de datos completas que contengan información confidencial.
  • Modificar o eliminar datos.
  • Ejecute operaciones administrativas en la base de datos.

Implementación segura

La manera segura de controlar las consultas SQL es usar consultas con parámetros (también denominadas instrucciones preparadas).

Por ejemplo:

// SECURE: Using parameterized queries
string query = "SELECT * FROM Users WHERE Username = @username AND Password = @password";
SqlCommand command = new SqlCommand(query, connection);
command.Parameters.AddWithValue("@username", userInput);
command.Parameters.AddWithValue("@password", passwordInput);
SqlDataReader reader = command.ExecuteReader();

Las consultas con parámetros separan el código de los datos. La base de datos solo trata los valores de parámetro como datos, nunca como código SQL ejecutable, lo que impide los ataques por inyección.

Otros tipos de inyección

La inyección de código SQL es solo una forma de ataque por inyección y los desarrolladores deben tener en cuenta otras vulnerabilidades de inyección que pueden poner en peligro la seguridad de las aplicaciones.

Aunque la inyección de CÓDIGO SQL es la más común, existen otras vulnerabilidades de inyección:

  • Inserción de comandos: inserción de comandos del sistema en entradas de aplicación que ejecutan comandos de shell.
  • Inyección ligera del Protocolo de acceso a directorios (LDAP): manipulación de consultas LDAP para acceder a información de directorio no autorizada.
  • Inserción de NoSQL: aprovechar las bases de datos NoSQL a través de consultas malintencionadas.
  • Inserción de XML: inserción de contenido XML malintencionado para acceder o modificar datos.

Patrón universal: cada vez que usted inserte una entrada no confiable en un comando o consulta que se interprete, corre el riesgo de inyección. El patrón de solución siempre es similar: sanear, validar o parametrizar para separar el código de los datos.

Cifrado débil de datos confidenciales

Almacenar o transmitir datos confidenciales sin el cifrado adecuado lo expone al acceso no autorizado. Esta categoría incluye métodos de cifrado inadecuados y la falta completa de cifrado.

Almacenamiento de contraseñas no seguro

Las contraseñas requieren protección especial porque sirven como mecanismo de autenticación principal para la mayoría de las aplicaciones.

Almacenar contraseñas incorrectamente es una vulnerabilidad crítica.

Almacenamiento de texto no cifrado (nunca aceptable)

// DANGEROUS: Storing passwords in plaintext
string password = userInput;
database.SavePassword(username, password);

Si la base de datos está en peligro, todas las contraseñas de usuario se exponen inmediatamente.

Hash débil (insuficiente)

// INSUFFICIENT: Using MD5 or SHA1 without salt
using (MD5 md5 = MD5.Create())
{
    byte[] hash = md5.ComputeHash(Encoding.UTF8.GetBytes(password));
    string hashedPassword = Convert.ToBase64String(hash);
}

MD5 y SHA1 se rompen criptográficamente. Las GPU modernas pueden probar miles de millones de combinaciones de contraseñas por segundo en estos hashes rápidos. Además, sin sal, los atacantes pueden utilizar tablas arcoíris precalculadas para descifrar contraseñas al instante.

// SECURE: Using bcrypt with automatic salt generation
string hashedPassword = BCrypt.Net.BCrypt.HashPassword(password);

// Later, for verification:
bool isValid = BCrypt.Net.BCrypt.Verify(userInput, storedHash);

El hash de contraseña seguro requiere:

  • Sal: datos aleatorios agregados a las contraseñas antes del hash, lo que impide los ataques de tabla arcoíris.
  • Algoritmo lento: las funciones como bcrypt, scrypt o Argon2 son costosas a nivel computacional, lo que limita los intentos de fuerza bruta a cientos o miles por segundo en lugar de miles de millones.

Cifrado de datos en reposo

Más allá de la seguridad de contraseñas, cualquier dato confidencial almacenado en disco o en bases de datos necesita protección a través del cifrado adecuado.

Los datos confidenciales almacenados sin cifrado son vulnerables si los medios de almacenamiento están en peligro.

Escenario vulnerable

// VULNERABLE: Writing sensitive data in plaintext
File.WriteAllText("customer_data.txt", sensitiveInformation);

Si se roba un portátil que contiene este archivo o si un atacante obtiene acceso al sistema de archivos, los datos se pueden leer inmediatamente.

Enfoque seguro

// SECURE: Encrypting data before storage
using (Aes aes = Aes.Create())
{
    aes.Key = GetEncryptionKey(); // Securely managed key
    aes.GenerateIV();
    
    using (FileStream fileStream = new FileStream("customer_data.enc", FileMode.Create))
    {
        fileStream.Write(aes.IV, 0, aes.IV.Length);
        using (CryptoStream cryptoStream = new CryptoStream(fileStream, aes.CreateEncryptor(), CryptoStreamMode.Write))
        using (StreamWriter writer = new StreamWriter(cryptoStream))
        {
            writer.Write(sensitiveInformation);
        }
    }
}

El cifrado adecuado proporciona una capa de defensa incluso si el almacenamiento está en peligro, suponiendo que las claves de cifrado se administren correctamente por separado.

Problemas de registro y control de errores

El registro incorrecto y el control de errores pueden exponer accidentalmente información confidencial o detalles del sistema que ayudan a los atacantes.

Registro de datos confidenciales

Aunque el registro es esencial para la depuración y la supervisión, puede convertirse en una vulnerabilidad de seguridad cuando se captura información confidencial.

Las aplicaciones nunca deben registrar información confidencial en texto no cifrado.

Prácticas de registro peligrosas

// DANGEROUS: Logging sensitive information
logger.LogInformation($"User {username} logged in with password: {password}");
logger.LogInformation($"Credit card processed: {cardNumber}");
logger.LogInformation($"API Key: {apiKey}");

Este código expone datos confidenciales en los registros, que podrían ser accesibles para usuarios no autorizados o filtrados a través de sistemas de administración de registros.

Procedimientos de registro seguros

// SECURE: Logging without sensitive data
logger.LogInformation($"User {username} logged in successfully");
logger.LogInformation($"Payment processed for order {orderId}");
logger.LogInformation($"API call authenticated successfully");

procedimientos recomendados

  • Nunca registre contraseñas, tokens de autenticación o claves de API.
  • Enmascara o redacta información confidencial, como números de tarjeta de crédito o números de seguridad social.
  • Registrar eventos y resultados, no valores de datos confidenciales.

Divulgación excesiva de información de error

Los mensajes de error sirven para un propósito de depuración importante, pero deben diseñarse cuidadosamente para evitar revelar los elementos internos del sistema a los posibles atacantes.

Los mensajes de error detallados pueden revelar la arquitectura del sistema, las rutas de acceso de archivo, los esquemas de base de datos y otra información útil para los atacantes.

Manejo de errores problemáticos

// PROBLEMATIC: Exposing detailed error information to users
catch (Exception ex)
{
    return $"Error: {ex.Message}\nStack Trace: {ex.StackTrace}\nConnection String: {connectionString}";
}

Esto revela los detalles internos del sistema que los atacantes pueden usar para crear ataques más sofisticados.

Control de errores seguro

// SECURE: User-friendly messages with detailed internal logging
catch (Exception ex)
{
    logger.LogError(ex, "Failed to process user request");
    return "An error occurred while processing your request. Please try again or contact support.";
}

Los usuarios reciben mensajes de error descriptivos y mínimos mientras los desarrolladores obtienen información detallada sobre los errores a través de registros seguros.

Ataques de recorrido de ruta

El recorrido de ruta de acceso (también denominado recorrido de directorio) se produce cuando una aplicación usa la entrada proporcionada por el usuario para construir rutas de acceso de archivo sin una validación adecuada. Los atacantes pueden usar secuencias de caracteres especiales para acceder a archivos fuera del directorio previsto.

Tenga en cuenta el código vulnerable siguiente:

// VULNERABLE: Using user input directly in file paths
string filename = Request.Query["file"];
string filePath = Path.Combine(@"C:\uploads\", filename);
string content = File.ReadAllText(filePath);

Un atacante podría proporcionar entradas como ../../../Windows/System32/config/SAM acceder a archivos del sistema confidenciales o ../../web.config leer la configuración de la aplicación que contiene secretos.

El código vulnerable habilita el siguiente mecanismo de ataque:

  • .. secuencias navegan hacia arriba por los niveles de directorio.
  • Los atacantes pueden escapar del espacio aislado del directorio previsto.
  • Acceda a archivos confidenciales, archivos de configuración o archivos del sistema.
  • Los archivos críticos de la aplicación podrían sobrescribirse.

Tenga en cuenta la siguiente implementación segura:

// SECURE: Validating and constraining file paths
string filename = Request.Query["file"];

// Remove path traversal sequences
filename = Path.GetFileName(filename);

// Construct full path
string uploadsDirectory = Path.GetFullPath(@"C:\uploads\");
string filePath = Path.GetFullPath(Path.Combine(uploadsDirectory, filename));

// Verify the resulting path is still within the uploads directory
if (!filePath.StartsWith(uploadsDirectory))
{
    throw new SecurityException("Invalid file path");
}

string content = File.ReadAllText(filePath);

Las implementaciones seguras muestran las siguientes estrategias de defensa:

  • Use Path.GetFileName() para quitar la información del directorio.
  • Permitir archivos o patrones en la lista de permitidos en lugar de incluir caracteres peligrosos en la lista de bloqueados.
  • Valide que las rutas de acceso resueltas permanezcan dentro de los directorios previstos.
  • Implemente permisos estrictos de acceso a archivos en el nivel de sistema operativo.

Otras consideraciones de seguridad

Además de las vulnerabilidades que se tratan en las secciones anteriores, otros problemas de seguridad requieren reconocimiento del desarrollador.

Cross-site scripting (scripting entre sitios, XSS)

El scripting entre sitios permite a los atacantes insertar código malintencionado en aplicaciones web, lo que podría poner en peligro los datos y las sesiones del usuario.

Aunque no es aplicable a las aplicaciones de consola, los desarrolladores web deben validar y codificar toda la entrada del usuario antes de mostrarla en exploradores.

Secretos codificados de forma rígida

Las credenciales y los valores de configuración confidencial insertados directamente en el código fuente representan un riesgo de seguridad crítico que puede exponer sistemas completos.

La inserción de claves de API, contraseñas o tokens directamente en el código fuente los expone a cualquier persona con acceso al repositorio. Los secretos deben ser:

  • Almacenamiento en sistemas de configuración seguros o bóvedas.
  • Nunca se ha comprometido con el control de versiones.
  • Se rota regularmente.
  • Administrado con controles de acceso adecuados.

Agotamiento de recursos y denegación de servicio

Los atacantes suelen aprovechar las aplicaciones que no administran correctamente los recursos. Los ataques pueden provocar interrupciones del servicio o bloqueos del sistema.

La administración de recursos deficiente puede habilitar ataques por denegación de servicio. Algunos ejemplos son:

  • Leer archivos grandes completos en la memoria (lo que provoca errores de memoria insuficiente).
  • No limitar los tamaños o frecuencias de las solicitudes.
  • Algoritmos ineficaces que consumen una CPU excesiva.
  • Fallar en gestionar los recursos adecuadamente.

Identificación de problemas de seguridad en el código

La identificación de vulnerabilidades de seguridad en el código requiere un enfoque sistemático.

Análisis del control de entradas de usuario

La entrada del usuario representa el vector de ataque principal para la mayoría de las vulnerabilidades de seguridad, lo que hace que sea fundamental examinar cómo procesa el código los datos externos.

Cada punto en el que el código acepta la entrada del usuario es un posible punto de entrada para ataques:

  • Buscar: entradas utilizadas en consultas SQL, rutas de archivos, comandos del sistema o lógica crítica.
  • Pregunte: "¿Estoy confiando en esta entrada demasiado?"
  • Considere: Vulnerabilidades de inyección, travesía de directorios, inyección de comandos.

Revisión de las operaciones criptográficas

Las implementaciones de seguridad que implican cifrado, hash y autenticación requieren un examen adicional porque la criptografía débil puede poner en peligro todos los sistemas.

El código criptográfico requiere un examen especial:

  • Buscar: MD5.Create(), SHA1.Create(), almacenamiento de contraseñas en texto no cifrado.
  • Pregunte: "¿Este método criptográfico todavía se considera seguro?"
  • Considere: Usar bcrypt, scrypt o Argon2 para contraseñas; SHA-256 o superior para las comprobaciones de integridad.

Examinar las declaraciones de registro

Los registros pueden convertirse involuntariamente en vulnerabilidades de seguridad cuando capturan información confidencial que debe permanecer protegida.

Analiza tu código base en busca de datos confidenciales en los logs.

  • Buscar: declaraciones de registro que contengan variables denominadas contraseña, secreto, token, apiKey y cardNumber.
  • Pregunte: "¿Qué información estoy exponiendo en los registros?"
  • Considere: ¿Qué ocurre si estos registros están en peligro o se exponen accidentalmente?

Inspección de operaciones de archivos

El código de control de archivos presenta desafíos de seguridad únicos porque puede exponer recursos del sistema más allá del ámbito previsto de la aplicación.

El código de control de archivos necesita una validación cuidadosa:

  • Buscar: Path.Combine con la entrada del usuario, operaciones de archivo basadas en rutas proporcionadas por el usuario.
  • Pregunte: "¿Puede un usuario escapar del directorio previsto?"
  • Considere: Ataques transversales de ruta y técnicas de escape de directorio.

Uso de herramientas automatizadas

Aunque la revisión manual de código es esencial, las herramientas automatizadas pueden examinar eficazmente grandes bases de código y identificar patrones de vulnerabilidad comunes que podrían perderse durante la inspección manual.

Combine la revisión manual del código con el análisis automatizado:

  • Análisis estático: herramientas como GitHub CodeQL escanean el código en busca de patrones de vulnerabilidad conocidos.
  • GitHub Copilot: Use el modo Preguntar para analizar secciones de código: "¿Hay problemas de seguridad en este código?"
  • Linter de seguridad: las herramientas específicas del lenguaje pueden señalar errores de seguridad evidentes.

GitHub Copilot puede identificar muchos problemas de seguridad comunes cuando se le pide que analice el código. Se basa en patrones de millones de código base para reconocer vulnerabilidades.

Enfoque de seguridad de desplazamiento a la izquierda

El principio de "desplazamiento hacia la izquierda" significa abordar la seguridad anteriormente en el ciclo de vida de desarrollo:

  • Fase de diseño: considere las implicaciones de seguridad de las decisiones arquitectónicas.
  • Fase de desarrollo: escriba código seguro desde el principio. Detectar problemas durante la revisión del código.
  • Fase de prueba: incluya pruebas de seguridad junto con pruebas funcionales.
  • Fase de implementación: busque vulnerabilidades antes de la versión.

Detectar problemas de seguridad durante el desarrollo es mucho menos costoso que detectarlos en producción. El costo de corregir vulnerabilidades aumenta exponencialmente con cada fase a la que avanzan.

Resumen

Las vulnerabilidades de seguridad comunes, como los ataques por inyección, el cifrado débil, el registro incorrecto y el recorrido de ruta de acceso representan amenazas graves para la seguridad de las aplicaciones. Comprender estas vulnerabilidades le ayuda a reconocerlas en el código y priorizar su corrección. Mediante la combinación de conocimientos de patrones de vulnerabilidad comunes con herramientas como GitHub Copilot, puede identificar y solucionar problemas de seguridad de forma más eficaz.