La programación defensiva es una forma de diseño defensivo destinada a desarrollar programas capaces de detectar posibles anomalías de seguridad y dar respuestas predeterminadas. [1] Garantiza el funcionamiento continuo de un 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: reducción del 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 comporte de manera predecible a pesar de entradas o acciones inesperadas del usuario.
Sin embargo, una programación excesivamente defensiva puede proteger contra errores que nunca se producirán, lo que generará costos de tiempo de ejecución y mantenimiento.
Programación segura
La programación segura es el subconjunto de la programación defensiva que se ocupa de la seguridad informática . La seguridad es lo que interesa, no necesariamente la seguridad o la disponibilidad (se puede permitir que el software falle de determinadas maneras). Como ocurre con todos los tipos de programación defensiva, evitar los errores es un objetivo principal; sin embargo, la motivación no es tanto reducir la probabilidad de fallos en el funcionamiento normal (como si la seguridad fuera la preocupación), sino reducir la superficie de ataque: el programador debe asumir que el software podría utilizarse de forma indebida para revelar errores y que estos podrían explotarse de forma maliciosa.
int risky_programming ( char * entrada ) { char str [ 1000 ]; // ... strcpy ( str , entrada ); // Copiar entrada. // ... }
La función tendrá un comportamiento indefinido cuando la entrada supere los 1000 caracteres. Es posible que algunos programadores no consideren que esto sea 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 . A continuación, se muestra una solución para este ejemplo:
int secure_programming ( char * input ) { char str [ 1000 + 1 ]; // Uno más para el carácter nulo.
// ...
// Copiar la entrada sin exceder la longitud del destino.
strncpy ( str , input , sizeof ( str ));
// Si strlen(input) >= sizeof(str) entonces strncpy no terminará en nulo.
// Contrarrestamos esto estableciendo siempre el último carácter en el buffer en NUL, // recortando efectivamente 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 largo. str [ sizeof ( str ) - 1 ] = '\0' ;
// ...
}
Programación ofensiva
La programación ofensiva es una categoría de programación defensiva, con el énfasis añadido de que ciertos errores no deben manejarse defensivamente . En esta práctica, solo se deben manejar 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 * nombre_color_semáforo ( enum color_semáforo c ) { switch ( c ) { case SEMÁFORO_ROJO : return "rojo" ; case SEMÁFORO_AMARILLO : return "amarillo" ; case SEMÁFORO_VERDE : return "verde" ; } return "negro" ; // Se manejará como un semáforo muerto. // Advertencia: Esta última declaración 'return' será descartada por un compilador optimizador // si todos los valores posibles de 'color_semáforo' están listados en // la declaración 'switch' anterior... }
- Programación ofensiva
const char * Trafficlight_colorname ( enum Trafficlight_color c ) { switch ( c ) { case TRAFFICLIGHT_RED : return "red" ; case TRAFFICLIGHT_YELLOW : return "yellow" ; case TRAFFICLIGHT_GREEN : return "green" ; } assert ( 0 ); // Afirma que esta sección es inalcanzable. // Advertencia: Esta llamada a la función 'assert' será descartada por un compilador optimizador // si todos los valores posibles de 'traffic_light_color' están listados en // la declaración 'switch' anterior... }
Confiar en los componentes del software
- Programación excesivamente defensiva
if ( is_legacy_compatible ( user_config )) { // Estrategia: No confíe en que el nuevo código se comporte de la misma manera que old_code ( user_config ); } else { // Alternativa: No confíe en que el nuevo código maneje los mismos casos if ( new_code ( user_config ) != OK ) { old_code ( user_config ); } }
- Programación ofensiva
// Espere que el nuevo código no tenga errores nuevos
if ( new_code ( user_config ) != OK ) { // Informe en voz alta y finalice abruptamente el programa para obtener la atención adecuada report_error ( "Algo salió muy mal" ); exit ( -1 ); }
Técnicas
A continuación se presentan algunas técnicas de programación defensiva:
Reutilización inteligente del código fuente
Si se prueba el código existente y se sabe que funciona, reutilizarlo puede reducir la posibilidad de que se introduzcan errores.
Sin embargo, la reutilización de código no siempre es una buena práctica. La reutilización de código existente, especialmente cuando se distribuye ampliamente, puede permitir la creación de vulnerabilidades dirigidas a un público más amplio de lo que sería posible de otro modo y conlleva toda la seguridad y las vulnerabilidades del código reutilizado.
Al considerar el uso de código fuente existente, una revisión rápida de los módulos (subsecciones como clases o funciones) ayudará a eliminar o a que el desarrollador esté al tanto de cualquier vulnerabilidad potencial y garantizará que sea adecuado para su uso en el proyecto. [ cita requerida ]
Problemas heredados
Antes de reutilizar código fuente antiguo, bibliotecas, API, 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 con esos requisitos en mente.
Muchos productos de software han experimentado problemas con el código fuente heredado antiguo; por ejemplo:
- Es posible que el código heredado no haya sido diseñado bajo una iniciativa de programación defensiva y, por lo tanto, podría ser de mucha menor calidad que el código fuente recientemente diseñado.
- Es posible que el código heredado se haya escrito y probado en condiciones que ya no son aplicables. Es posible que las antiguas pruebas de control de calidad ya no tengan validez.
- Ejemplo 1 : el código heredado puede haber sido diseñado para la entrada ASCII, pero ahora la entrada es UTF-8.
- Ejemplo 2 : el código heredado puede haber sido compilado y probado en arquitecturas de 32 bits, pero al compilarse en arquitecturas de 64 bits, pueden surgir nuevos problemas aritméticos (por ejemplo, pruebas de signo no válidas, conversiones de tipos no válidas, etc.).
- Ejemplo 3 : Es posible que el código heredado haya sido diseñado para máquinas fuera de línea, pero se vuelve vulnerable una vez que se agrega conectividad de red.
- El código heredado no se escribe teniendo en cuenta nuevos problemas. Por ejemplo, es probable que el código fuente escrito en 1990 sea propenso a muchas vulnerabilidades de inyección de código , porque la mayoría de esos problemas no se entendían ampliamente en ese momento.
Ejemplos notables del problema del legado:
- 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", [2] nombrando la seguridad, la robustez, la escalabilidad y los nuevos protocolos como preocupaciones clave para reescribir el antiguo código heredado.
- Microsoft Windows sufrió la vulnerabilidad "Windows Metafile " y otras vulnerabilidades relacionadas con el formato WMF. El Centro de Respuesta de Seguridad de Microsoft describe las características de WMF como "Alrededor de 1990, se agregó soporte para WMF... Esta era una época diferente en el panorama de seguridad... todos eran completamente confiables" [3] , no siendo desarrollados bajo las iniciativas de seguridad de Microsoft.
- Oracle está combatiendo problemas heredados, como el código fuente antiguo escrito sin abordar las preocupaciones de inyección SQL y escalada de privilegios , lo que resulta en muchas vulnerabilidades de seguridad que han llevado tiempo para solucionar y también han generado correcciones incompletas. Esto ha dado lugar a fuertes críticas de expertos en seguridad como David Litchfield , Alexander Kornbrust, Cesar Cerrudo. [4] [5] [6] Una crítica adicional es que las instalaciones predeterminadas (en gran parte un legado de versiones anteriores) 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 las configuraciones heredadas menos seguras 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 pirata informático podría pasar otra variante de este nombre de archivo, como "/etc/./passwd". Se pueden emplear bibliotecas de canonización para evitar errores debido a entradas no canónicas .
Baja tolerancia frente a errores "potenciales"
Supongamos que las construcciones de código que parecen ser propensas a problemas (similares a vulnerabilidades conocidas, etc.) son errores y posibles fallas de seguridad. La regla básica es: "No estoy al tanto de todos los tipos de vulnerabilidades de seguridad . Debo protegerme contra aquellas que conozco y luego debo ser proactivo".
Otros consejos para proteger tu código
- Uno de los problemas más comunes es el uso sin control de estructuras de tamaño constante o preasignadas para datos de tamaño dinámico [ cita requerida ] como entradas al programa (el problema del desbordamiento del búfer ). Esto es especialmente común para datos de cadena en C [ cita requerida ] . Las funciones de la biblioteca de C como
getsnunca se deben usar ya que el tamaño máximo del búfer de entrada no se pasa como argumento. Las funciones de la biblioteca de C comoscanfse pueden usar de forma segura, pero requieren que el programador tenga cuidado con la selección de cadenas de formato seguro, depurándolas antes de usarlas. - Cifre o autentique todos los datos importantes que se transmiten a través de las redes. No intente implementar su propio esquema de cifrado, utilice uno que ya haya sido probado . La verificación de mensajes con CRC o tecnología similar también ayudará a proteger los datos que se envían a través de una red.
Las tres reglas de seguridad de 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 código es inseguro hasta que se demuestre lo contrario.
- No se puede probar la seguridad de ningún código en el espacio de usuario , o, más comúnmente conocido como: "nunca confíes en el cliente" .
Estas tres reglas sobre seguridad de datos describen cómo manejar cualquier dato, de origen interno o externo:
Todos los datos son importantes hasta que se demuestre lo contrario , lo que significa que todos los datos deben verificarse como basura antes de ser destruidos.
Todos los datos están contaminados hasta que se demuestre lo contrario , lo que significa que todos los datos deben manejarse de una manera que no exponga el resto del entorno de ejecución sin verificar su integridad.
Todo código es inseguro hasta que se demuestre lo contrario . Si bien es un nombre ligeramente inapropiado, es un buen trabajo recordarnos que nunca debemos asumir que nuestro código es seguro, ya que los errores o el comportamiento indefinido pueden exponer el proyecto o sistema a ataques como los ataques de inyección SQL comunes .
Más información
- Si se debe verificar la exactitud de los datos, verifique que sean correctos, no que sean incorrectos.
- Diseño por contrato
- Afirmaciones (también llamadas programación asertiva )
- Preferir excepciones a códigos de retorno
- En términos generales, es preferible [ ¿según quién? ] lanzar mensajes de excepción que hagan cumplir parte de su contrato de API y guíen al desarrollador en lugar de devolver valores de código de error que no apuntan a dónde ocurrió la excepción o cómo se veía la pila del programa. Un mejor registro y manejo de excepciones aumentará la solidez y seguridad de su software [ cita requerida ] , al tiempo que minimiza el estrés del desarrollador [ cita requerida ] .
Véase también
Referencias
- ^ Boulanger, Jean-Louis (1 de enero de 2016), Boulanger, Jean-Louis (ed.), "6 - Técnica para gestionar la seguridad del software", Aplicaciones de software certificables 1 , Elsevier, págs. 125-156, ISBN 978-1-78548-117-8, consultado el 2 de septiembre de 2022
- ^ "Archivo fogo: Paul Vixie y David Conrad sobre BINDv9 y la seguridad en Internet por Gerald Oskoboiny <gerald@impression.net>". impressions.net . Consultado el 27 de octubre de 2018 .
- ^ "Si analizamos el problema de WMF, ¿cómo llegó hasta ahí?". MSRC . Archivado desde el original el 24 de marzo de 2006. Consultado el 27 de octubre de 2018 .
- ^ Litchfield, David. "Bugtraq: Oracle, ¿dónde están los parches?". seclists.org . Consultado el 27 de octubre de 2018 .
- ^ Alexander, Kornbrust. "Bugtraq: RE: Oracle, ¿dónde están los parches?". seclists.org . Consultado el 27 de octubre de 2018 .
- ^ Cerrudo, Cesar. "Bugtraq: Re: [Divulgación completa] RE: Oracle, ¿dónde están los parches???". seclists.org . Consultado el 27 de octubre de 2018 .
Enlaces externos
- Estándares de codificación segura de CERT