La programación defensiva es una forma de diseño defensivo destinada a garantizar el funcionamiento continuo 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: reduce la cantidad 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 se apruebe en una auditoría de código .
- Hacer que el software se comporte de una manera predecible a pesar de entradas inesperadas o acciones del usuario.
La programación demasiado defensiva, sin embargo, puede proteger contra errores que nunca se encontrarán, incurriendo así en costos de mantenimiento y tiempo de ejecución. También existe el riesgo de que las capturas de código eviten demasiadas excepciones , lo que podría generar resultados incorrectos e inadvertidos.
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 la preocupación, no necesariamente la seguridad o la disponibilidad ( se puede permitir que el software falle de ciertas maneras). Como ocurre con todo tipo de programación defensiva, evitar errores es un objetivo primordial; sin embargo, la motivación no es tanto reducir la probabilidad de fallas 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 puede ser utilizado de forma incorrecta de forma activa para revelar errores, y que Los errores pueden explotarse de forma maliciosa.
int risky_programming ( char * input ) { char str [ 1000 ]; // ... strcpy ( str , entrada ); // Copiar entrada. // ... }
La función dará como resultado un comportamiento indefinido cuando la entrada tenga más de 1000 caracteres. Algunos programadores novatos pueden no sentir que esto es un problema, suponiendo que ningún usuario ingrese 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 ]; // Uno más para el carácter nulo. // ... // Copia 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 búfer 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 agregado de que ciertos errores no deben manejarse a la defensiva . 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 de la línea de defensa del programa .
Confiar en la validez de los datos internos
- Programación demasiado defensiva
const char * trafficlight_colorname ( enumeración traffic_light_color c ) { switch ( c ) { case TRAFFICLIGHT_RED : return "rojo" ; case TRAFFICLIGHT_YELLOW : return "amarillo" ; case TRAFFICLIGHT_GREEN : return "verde" ; } return "negro" ; // Para ser manejado como un semáforo apagado. // Advertencia: Esta última sentencia 'return' será descartada por un // compilador de optimización si todos los valores posibles de 'traffic_light_color' se enumeran en // la sentencia 'switch' anterior ... }
- Programación ofensiva
const char * trafficlight_colorname ( enumeración traffic_light_color c ) { switch ( c ) { case TRAFFICLIGHT_RED : return "rojo" ; case TRAFFICLIGHT_YELLOW : return "amarillo" ; case TRAFFICLIGHT_GREEN : return "verde" ; } afirmar ( 0 ); // Afirmar que esta sección es inalcanzable. // Advertencia: esta llamada a la función 'aseverar' será descartada por un // compilador de optimización si todos los valores posibles de 'traffic_light_color' se enumeran en // la declaración 'switch' anterior ... }
Confiar en los componentes del software
- Programación demasiado defensiva
if ( is_legacy_compatible ( user_config )) { // Estrategia: No confíe en que el nuevo código se comporta con el mismo old_code ( user_config ); } else { // Fallback: No confíe en que el nuevo código maneja 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" ); salir ( -1 ); }
Técnicas
A continuación, se muestran 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, reutilizar el código no siempre es una buena práctica, porque también amplifica los daños de un posible ataque al código inicial. La reutilización en este caso puede causar errores graves en los procesos comerciales . [ aclarar ]
Problemas heredados
Antes de reutilizar el código fuente antiguo, las bibliotecas, las API, las configuraciones, etc., se debe considerar si el trabajo anterior 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 o probaron con esos requisitos en mente.
Muchos productos de software han experimentado problemas con el antiguo código fuente heredado; por ejemplo:
- Es posible que el código heredado no se haya 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 recién diseñado.
- Es posible que el código heredado se haya escrito y probado en condiciones que ya no se aplican. Es posible que las antiguas pruebas de garantía de calidad ya no tengan validez.
- Ejemplo 1 : es posible que el código heredado se haya 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 cuando se compila en arquitecturas de 64 bits, pueden ocurrir nuevos problemas aritméticos (por ejemplo, pruebas de firma inválida, conversiones de tipos inválidos, etc.).
- Ejemplo 3 : el código heredado puede haber sido dirigido a máquinas fuera de línea, pero se vuelve vulnerable una vez que se agrega la conectividad de red.
- El código heredado no está escrito con nuevos problemas en mente. 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 estos 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", [1] nombrando la seguridad, robustez, escalabilidad y nuevos protocolos como preocupaciones clave para reescribir código antiguo.
- Microsoft Windows sufrió "la" vulnerabilidad de metarchivo de Windows 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ó el soporte de WMF ... Este era un momento diferente en el panorama de la seguridad ... todos eran completamente confiables" , [2] no se desarrolló bajo las iniciativas de seguridad en Microsoft.
- Oracle está combatiendo problemas heredados, como el código fuente antiguo escrito sin abordar las preocupaciones de la inyección SQL y la escalada de privilegios , lo que genera muchas vulnerabilidades de seguridad que han tardado en solucionarse y también han generado correcciones incompletas. Esto ha dado lugar a fuertes críticas por parte de expertos en seguridad como David Litchfield , Alexander Kornbrust , Cesar Cerrudo . [3] [4] [5] 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 la Lista de verificación de seguridad de la base de datos de Oracle , que es difícil de modificar ya que muchas aplicaciones requieren la configuraciones heredadas menos seguras para que funcionen 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". Canonicalización bibliotecas se pueden emplear para insectos Evita debido a la no canónica de entrada.
Baja tolerancia contra errores "potenciales"
Suponga que las construcciones de código que parecen ser propensas a problemas (similares a las 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 que deben proteger contra los que yo. No conozco y entonces debo ser proactivo!".
Otras tecnicas
- Uno de los problemas más comunes es el uso no verificado de estructuras y funciones de tamaño constante para datos de tamaño dinámico (el problema de desbordamiento del búfer ). Esto es especialmente común para cadena de datos en C . Las funciones de la biblioteca C como gets nunca deben usarse ya que el tamaño máximo del búfer de entrada no se pasa como argumento. Las funciones de la biblioteca C como scanf se pueden usar de forma segura, pero requieren que el programador tenga cuidado con la selección de cadenas de formato seguro, desinfectando antes de usarlas.
- Cifre / autentique todos los datos importantes transmitidos a través de las redes. No intente implementar su propio esquema de cifrado, utilice uno probado en su lugar.
- 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.
- No puede probar la seguridad de ningún código en el área de usuario , o, más canónicamente: "nunca confíe en el cliente" .
- Si se va a verificar la exactitud de los datos, verifique que sean correctos, no que sean incorrectos.
- Diseño por contrato
- El diseño por contrato utiliza precondiciones , poscondiciones e invariantes para garantizar que los datos proporcionados (y el estado del programa en su conjunto) se desinfecten. Esto permite que el código documente sus suposiciones y las haga de forma segura. Esto puede implicar comprobar la validez de los argumentos de una función o método antes de ejecutar el cuerpo de la función. Después del cuerpo de una función, también es aconsejable realizar una verificación del estado u otros datos retenidos, y el valor de retorno antes de las salidas (ruptura / retorno / lanzamiento / código de error).
- Afirmaciones (también llamadas programación asertiva )
- Dentro de las funciones, es posible que desee comprobar que no está haciendo referencia a algo que no es válido (es decir, nulo) y que las longitudes de la matriz son válidas antes de hacer referencia a los elementos, especialmente en todas las instancias temporales / locales. Una buena heurística es no confiar en las bibliotecas que tampoco escribiste. Entonces, cada vez que los llame, verifique lo que recibe de ellos. A menudo ayuda crear una pequeña biblioteca de funciones de "afirmación" y "verificación" para hacer esto junto con un registrador para que pueda rastrear su ruta y reducir la necesidad de ciclos de depuración extensos en primer lugar. Con la llegada de las bibliotecas de registro y la programación orientada a aspectos , muchos de los aspectos tediosos de la programación defensiva se mitigan.
- Prefiere las excepciones a los códigos de retorno
- En términos generales, es preferible lanzar mensajes de excepción inteligibles que hagan cumplir parte de su contrato de API y guíen al programador del cliente en lugar de devolver valores para los que un programador del cliente probablemente no esté preparado y, por lo tanto, minimizar sus quejas y aumentar la solidez y seguridad de su software. . [ dudoso ]
Ver también
- La seguridad informática
- Programación consciente de la inmunidad
Referencias
- ^ "archivo de fogo: Paul Vixie y David Conrad sobre BINDv9 e Internet Security por Gerald Oskoboiny "@impressive.net> . impresionante.net . Consultado el 27 de octubre de 2018 .
- ^ "Mirando el problema de WMF, ¿cómo llegó allí?" . 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 CERT