Programación defensiva
programación defensiva es una forma de diseño defensivo destinada a desarrollar programas que sean capaces de detectar posibles anomalías de seguridad y generar respuestas predeterminadas. Asegura la función continua de una pieza de software en circunstancias imprevistas. Las prácticas de programación defensiva se utilizan a menudo cuando se necesita alta disponibilidad, seguridad o protección.
La programación defensiva es un enfoque para mejorar el software y el código fuente, en términos de:
- Calidad general – reduciendo el número de errores y problemas de software.
- Hacer que el código fuente sea comprensible – el código fuente debe ser legible y comprensible para que sea aprobado en una auditoría de código.
- Hacer que el software se comporta de manera predecible a pesar de entradas inesperadas o acciones del usuario.
Sin embargo, la programación demasiado defensiva puede proteger contra errores que nunca se encontrarán, lo que genera costos de tiempo de ejecución y mantenimiento. También existe el riesgo de que las trampas de código impidan demasiadas excepciones, lo que podría dar lugar a resultados incorrectos e inadvertidos.
Programación segura
La programación segura es el subconjunto de la programación defensiva relacionada con la seguridad informática. La seguridad es la preocupación, no necesariamente la seguridad o la disponibilidad (se puede permitir que el software falle de ciertas maneras). Al igual que con todo tipo de programación defensiva, evitar errores es un objetivo principal; sin embargo, la motivación no es tanto reducir la probabilidad de falla en la operación normal (como si la seguridad fuera la preocupación), sino reducir la superficie de ataque: el programador debe asumir que el software podría ser mal utilizado activamente para revelar errores y que los errores podrían explotarse maliciosamente.
int risky_programming()char *entrada) {} char str[1000]; //... strcpy()str, entrada); // Copia de entrada. //...}
La función dará como resultado un comportamiento indefinido cuando la entrada tenga más de 1000 caracteres. Algunos programadores pueden sentir que esto no es un problema, suponiendo que ningún usuario ingresará una entrada tan larga. Este error en particular demuestra una vulnerabilidad que permite explotar el desbordamiento del búfer. Aquí hay una solución a este ejemplo:
int secure_programming()char *entrada) {} char str[1000+1]; // Una más para el personaje nulo. //... // Copiar entrada sin exceder la longitud del destino. strncpy()str, entrada, tamaño()str)); // Si strlen(input) >= sizeof(str) entonces strncpy no terminará null. // Enfrentamos esto siempre colocando el último personaje en el búfer a NUL, // efectivamente cortar la cadena a la longitud máxima que podemos manejar. // También se puede decidir abortar explícitamente el programa si strlen(input) es - Demasiado tiempo. str[tamaño()str) - 1] = '0'; //...}
Programación ofensiva
La programación ofensiva es una categoría de programación defensiva, con el énfasis adicional de que ciertos errores no deben manejarse a la defensiva. En esta práctica, solo se manejarán los errores que estén fuera del control del programa (como la entrada del usuario); en esta metodología se debe confiar en el software en sí, así como en los datos dentro de la línea de defensa del programa.
Confiar en la validez de los datos internos
- Programación excesivamente defensiva
const char* trafficlight_colorname()enum traffic_light_color c) {} interruptor ()c) {} Caso TRAFFICLIGHT_RED: retorno "rojo"; Caso TRAFFICLIGHT_YELLOW: retorno "amarillo"; Caso TRAFFICLIGHT_GREEN: retorno "verde"; } retorno "negro"; // Para ser manejado como un semáforo muerto. // Advertencia: Esta última declaración de "retorno" será descartada por una optimización // compilador si todos los valores posibles de 'traffic_light_color' se enumeran en // la declaración anterior 'switch'...}
- Programación ofensiva
const char* trafficlight_colorname()enum traffic_light_color c) {} interruptor ()c) {} Caso TRAFFICLIGHT_RED: retorno "rojo"; Caso TRAFFICLIGHT_YELLOW: retorno "amarillo"; Caso TRAFFICLIGHT_GREEN: retorno "verde"; } afirmación()0); // Afirme que esta sección es inalcanzable. // Advertencia: Esta llamada de la función 'assert' será bajada por una optimización // compilador si todos los valores posibles de 'traffic_light_color' se enumeran en // la declaración anterior 'switch'...}
Confiar en los componentes de software
- Programación excesivamente defensiva
si ()is_legacy_compatible()user_config) {} // Estrategia: No confíes en que el nuevo código se comporta igual old_code()user_config);} más {} // Fallback: No confíe en que el nuevo código maneja los mismos casos si ()new_code()user_config) ! OK) {} old_code()user_config); }}
- Programación ofensiva
// Espera que el nuevo código no tenga nuevos erroressi ()new_code()user_config) ! OK) {} // Loudly report and abruptly terminate program to get proper attention report_error()"Algo salió muy mal"); Salida()-1);}
Técnicas
Aquí hay algunas técnicas de programación defensiva:
Reutilización de código fuente inteligente
Si se prueba el código existente y se sabe que funciona, reutilizarlo puede reducir la posibilidad de que se introduzcan errores.
Sin embargo, reutilizar código no es siempre una buena práctica. La reutilización del código existente, especialmente cuando se distribuye ampliamente, puede permitir la creación de exploits dirigidos a una audiencia más amplia de lo que sería posible de otro modo y trae consigo toda la seguridad y vulnerabilidades del código reutilizado.
Al considerar el uso del código fuente existente, una revisión rápida de los módulos (subsecciones como clases o funciones) ayudará a eliminar o alertará al desarrollador sobre cualquier vulnerabilidad potencial y garantizará que sea adecuado para su uso en el proyecto.
Problemas heredados
Antes de reutilizar el código fuente antiguo, las bibliotecas, las API, las configuraciones, etc., se debe considerar si el trabajo antiguo es válido para su reutilización o si es probable que sea propenso a problemas heredados.
Los problemas heredados son problemas inherentes cuando se espera que los diseños antiguos funcionen con los requisitos actuales, especialmente cuando los diseños antiguos no se desarrollaron ni probaron teniendo en cuenta esos requisitos.
Muchos productos de software han experimentado problemas con el código fuente heredado antiguo; Por ejemplo:
- El código de Legacy puede no haber sido diseñado bajo una iniciativa de programación defensiva, y por lo tanto podría ser de una calidad mucho menor que el código fuente de nuevo diseño.
- El código de Legacy puede haber sido escrito y probado en condiciones que ya no se aplican. Las viejas pruebas de garantía de calidad pueden ya no tener validez.
- Ejemplo 1: código legado puede haber sido diseñado para la entrada ASCII, pero ahora la entrada es UTF-8.
- Ejemplo 2: el código hereditario puede haber sido compilado y probado en arquitecturas de 32 bits, pero cuando se compiló en arquitecturas de 64 bits, pueden ocurrir nuevos problemas aritméticos (por ejemplo, pruebas de firma inválidas, moldes de tipo inválido, etc.).
- Ejemplo 3: el código hereditario puede haber sido apuntado para máquinas offline, pero se vuelve vulnerable una vez que se añade conectividad de red.
- El código de Legacy no está escrito con nuevos problemas en mente. Por ejemplo, es probable que el código fuente escrito en 1990 sea propensa a muchas vulnerabilidades de inyección de código, ya que la mayoría de esos problemas no se entendían ampliamente en ese momento.
Ejemplos notables del problema heredado:
- BIND 9, presentado por Paul Vixie y David Conrad como "BINDv9 es una reescritura completa", "La seguridad fue una consideración clave en el diseño", nombrando seguridad, robustez, escalabilidad y nuevos protocolos como preocupaciones clave para reescribir el antiguo código hereditario.
- Microsoft Windows sufrió de "la" vulnerabilidad de Windows Metafile y otras explotaciones relacionadas con el formato WMF. Microsoft Security Response Center describe las características de WMF como "En torno a 1990, se añadió apoyo WMF... Este era un momento diferente en el paisaje de seguridad... todos confiaban completamente", no siendo desarrollado bajo las iniciativas de seguridad en Microsoft.
- Oracle está combatiendo los problemas heredados, como el código fuente antiguo escrito sin abordar las preocupaciones de la inyección de SQL y la escalada de privilegios, lo que ha dado lugar a muchas vulnerabilidades de seguridad que han tomado tiempo para corregir y también generado correcciones incompletas. Esto ha dado lugar a fuertes críticas de expertos en seguridad como David Litchfield, Alexander Kornbrust, César Cerrudo. Una crítica adicional es que las instalaciones predeterminadas (en gran medida un legado de versiones antiguas) no están alineadas con sus propias recomendaciones de seguridad, como Oracle Database Security Checklist, que es difícil de modificar ya que muchas aplicaciones requieren la configuración heredada menos segura para funcionar correctamente.
Canonicalización
Es probable que los usuarios malintencionados inventen nuevos tipos de representaciones de datos incorrectos. Por ejemplo, si un programa intenta rechazar el acceso al archivo "/etc/passwd", un cracker podría pasar otra variante de este nombre de archivo, como "/etc/./passwd". Se pueden emplear bibliotecas de canonicalización para evitar errores debido a entradas no canónicas.
Baja tolerancia contra el "potencial" bichos
Suponga que las construcciones de código que parecen ser propensas a problemas (similares a vulnerabilidades conocidas, etc.) son errores y fallas de seguridad potenciales. La regla general básica es: "No estoy al tanto de todos los tipos de vulnerabilidades de seguridad. ¡Debo protegerme contra aquellos que sí conozco y luego debo ser proactivo!".
Otros consejos para asegurar su código
- Uno de los problemas más comunes es el uso incontrolado de estructuras de tamaño constante o preasignadas para datos de tamaño dinámico, como entradas al programa (el problema de desbordamiento del buffer). Esto es especialmente común para los datos de cadena en C. Funciones de la biblioteca C como
gets
nunca se debe utilizar ya que el tamaño máximo del buffer de entrada no se pasa como un argumento. Funciones de la biblioteca C comoscanf
se puede utilizar con seguridad, pero requiere que el programador se ocupe de la selección de cadenas de formato seguro, sanitizándola antes de utilizarla. - Encrypt/authenticate all important data transmitted over networks. No trate de implementar su propio esquema de cifrado, use uno probado en su lugar. Verificación de mensajes con CRC o tecnología similar también ayudará a asegurar datos enviados a través de una red.
Las 3 reglas de la seguridad de los datos
* Todos los datos son importantes hasta que se demuestre lo contrario. * Todos los datos están contaminados hasta que se demuestre lo contrario. * Todo el código es inseguro hasta que se demuestre lo contrario.
- Usted no puede demostrar la seguridad de cualquier código en el usuario, o, más comúnmente conocido como: "Nunca confíes en el cliente".
Estas tres reglas sobre la seguridad de los datos describen cómo manejar cualquier dato, de origen interno o externo:
Todos los datos son importantes hasta que se demuestre lo contrario: significa que todos los datos deben verificarse como basura antes de destruirse.
Todos los datos están contaminados hasta que se demuestre lo contrario: significa que todos los datos deben manejarse de una manera que no exponga el resto del entorno de tiempo de ejecución sin verificar la integridad.
Todo el código es inseguro hasta que se demuestre lo contrario: aunque es un nombre ligeramente inapropiado, hace un buen trabajo al recordarnos que nunca debemos asumir que nuestro código es seguro, ya que los errores o el comportamiento indefinido pueden exponer el proyecto o el sistema a ataques como como ataques comunes de inyección SQL.
Más información
- Si los datos deben ser revisados para la corrección, verifique que es correcto, no que sea incorrecto.
- Diseño por contrato
- Aserciones (también llamadas programación afirmativa)
- Preferir excepciones a los códigos de devolución
- En términos generales, es preferible lanzar mensajes de excepción que ejecuten parte de su contrato API y guíen al desarrollador en lugar de devolver valores de código de error que no apuntan a donde ocurrió la excepción o lo que parecía la pila de programa, Mejor registro y manejo de excepción aumentará la robustez y seguridad de su software, al minimizar el estrés del desarrollador.
Contenido relacionado
Iván Sutherland
Idempotencia
NTFS