Programación defensiva
La programación defensiva (defensive programming en inglés) es una forma de diseño defensivo aplicada al diseño de software que busca garantizar el comportamiento de todo elemento de una aplicación ante cualquier situación de uso por incorrecta o imprevisible que ésta pueda parecer. En general, esto supone multiplicar las comprobaciones que se realizan en todos los módulos programados, con la consiguiente penalización en carga de procesador, tiempo y aumento en la complejidad del código. Las técnicas de programación defensiva se utilizan especialmente en componentes críticos cuyo mal funcionamiento, ya sea por descuido o por ataque malicioso, podría acarrear consecuencias graves o daños catastróficos.
La programación defensiva es un enfoque que busca mejorar el software y el código fuente, en términos de:
- Calidad - reduciendo el número de fallos de software y, en consecuencia, problemas.
- Haciendo el código fuente comprensible - el código fuente debe ser legible y comprensible, a prueba de una auditoría de código.
- Hacer que el software se comporte de una manera predecible pese a entradas o acciones de usuario inesperadas.
Programación segura
La programación defensiva es algunas veces referida como programación segura por los científicos de computación los cuales ubican este enfoque minimizando errores de software.
Los errores de software (bugs) pueden ser potencialmente usados por un cracker (nombrados hacker por confusión) para una inyección de código, ataques de denegación de servicio u otro ataque.
Una diferencia entre programación defensiva y prácticas normales es que pocas hipótesis son hechas por el programador, el cual intenta manejar todos los posibles estados de error. En resumen, el programador nunca asume que una llamada a una función particular o biblioteca trabajará bajo las entradas previstas.
Técnicas de programación defensiva
Aquí están algunas técnicas de programación defensiva sugeridas por algunos de los científicos líderes de computación para evitar crear problemas de seguridad y errores de software. Estos científicos afirman que aunque este proceso pueda mejorar la calidad el código, no es suficiente para asegurar la seguridad.
Reducir complejidad del código fuente
Nunca hacer el código más complejo de lo necesario. La complejidad genera bugs, incluyendo problemas de seguridad. Esta meta puede tener conflicto con el objetivo de escribir programas que puedan recuperarse de cualquier error y manejar cualquier entrada de datos. Manejar todas las ocurrencias inesperadas en un programa requiere que el programador añada código extra, el cual pudiera también tener bugs.
Revisiones del código fuente
Una revisión de código es donde alguien diferente al autor original realiza una auditoría de código. Una auditoría de código hecha por el mismo creador es insuficiente. La auditoría la debe hacer alguien que no sea el autor, como cuando se escribe un libro, debe ser revisado por alguien que no sea el autor.
Simplemente haciendo el código disponible para que otros lo lean (software libre) es insuficiente, pues no hay garantía que el código sea efectivamente revisado, no dejando que sea rigurosamente revisado.
Pruebas de software
Las pruebas de software deberán ser para tanto que el software trabaje como debe, como cuando se supone que pase si se realice deliberadamente malas entradas.
Las herramientas de prueba pueden capturar entradas de teclado asociadas con operaciones normales. Luego las cadenas de texto de estas entradas capturadas pueden ser copiadas y editadas para ensayar todas las permutaciones y combinaciones, luego ampliarlas para tests posteriores después de cualquier modificación-. Los defensores de la clave de registro afirman que los programadores que usan este método deberán asegurar que las personas a las cuales se les están capturando las entradas estén al tanto de esto, y con que propósito?, para evitar acusaciones de violación de privacidad .
Reutilización inteligente del código fuente
Si es posible, reutilizar código. La idea es capturar beneficios de un bien escrito y bien probado código fuente, en vez de crear bugs innecesarios.
Sin embargo, reutilizar código no siempre es la mejor manera de progresar, particularmente cuando la lógica del negocio está involucrada. Reutilizar en este caso puede causar serios bugs en los procesos de negocio.
Los problemas de legado
Antes de reutilizar código fuente viejo, bibliotecas, APIs, configuración y demás, debe ser considerado si el trabajo anterior es válido para reutilizar, o si es propenso a problemas de legado.
Los problemas de legado son problemas inherentes cuando se espera que viejos diseños trabajen con los requerimientos actuales, especialmente cuando estos viejos diseños no fueron desarrollados o probados con estos requerimientos en mente.
Muchos productos de software han experimentado problemas con viejos códigos fuente legados, por ejemplo:
- El código legado puede no haber sido diseñado bajo iniciativa de Programación Defensiva, y puede por consiguiente ser de mucha menos calidad que un diseño más nuevo de código fuente.
- El código legado puede haber sido escrito y probado bajo condiciones que ya no aplican más. Los viejos test de aseguramiento de calidad pueden no ser válidos ahora. Ejemplo 1: El código legado puede haber sido diseñado para entradas ASCII pero ahora la entrada es UTF-8. Ejemplo 2: El código legado puede haber sido compilado y probado sobre arquitecturas de 32 bits, pero cuando es compilado sobre arquitecturas de 64 bits pueden ocurrir nuevos problemas aritméticos. Ejemplo 3: Un código legado puede haberse enfocado hacia máquinas fuera de línea, pero se vuelve vulnerable una vez la conectividad de red es adicionada.
- El código legado no es escrito con nuevos problemas en mente. Por ejemplo, código fuente escrito en 1990, puede ser propenso a vulnerabilidades de Inyección de Código, porque muchos de estos problemas no eran extensamente entendidos en esa época.
Ejemplos notables de problemas de legado:
- BIND 9, presentado por Paul Vixie y David Conrad como “BINDv9 es una completa reescritura”, “La seguridad fue una consideración clave en diseño”, nombrando seguridad, robustez, escalabilidad y nuevos protocolos como preocupaciones clave para reescribir viejo código legado.
- Microsoft Windows sufrió del “Windows Metafile Vulnerability” y otras explotaciones referentes al formato WMF, El centro de respuesta de seguridad de Microsoft describe que características de WMF como “ Alrededor de 1990, el soporte WMF fue adicionado…. Este fue un tiempo diferente en el paisaje de seguridad….estábamos todos completamente confiados….”.
- Oracle está combatiendo problemas de legado, como el viejo código fuente escrito sin direccional preocupaciones respecto a la inyección SQL y Escalas de Privilegio, resultando en muchas vulnerabilidades de seguridad las cuales han tomado tiempo en ser arregladas y han generado arreglos incompletos. Esto le ha levantado pesadas críticas de los expertos de seguridad como David Litchfield, Alexander Kombrust, Cesar Cerrado. Una crítica adicional es que las instalaciones por defecto (largamente un legado de viejas versiones) no se encuentra alineado con sus propias recomendaciones de seguridad, como el Oracle Database Security Checklist, el cual es difícil de enmendar.
Canonicalizar
Los crackers tienden a buscar representaciones diferentes de los mismos datos.
Por ejemplo, si un programa verifica un nombre de archivo contra /etc/passwd
, un cracker puede intentar usar un nombre diferente pero que se refiere al mismo archivo, como /etc/./passwd
.
Para evitar bugs debido a entradas no canónicas, se deben emplear las API de canonicalización.
Principio del menor privilegio
Emplear el principio del menor privilegio, Evitar tener software corriendo en un modo privilegiado:
- Nunca hacer programas UNIX setuid a menos que se esté realmente seguro que se está protegiendo la seguridad.
- Nunca hacer programas de Windows correr como servicios de sistema local a menos que se esté realmente seguro que se está protegiendo la seguridad.
- No conceder más permisos que los necesarios a grandes grupos de usuarios o público/cualquiera.
- No conceder más permisos que los necesarios a grupos pequeños de usuarios o usuarios específicos.
- Preferir conceder permisos a grupos pequeños de usuarios o usuarios específico, en vez de concederlos a grandes grupos de usuarios o público/cualquiera. Es mejor que unos pocos usuarios tengan mas permisos, que muchos usuarios tengan mayor permiso.
Baja tolerancia contra errores potenciales
Asumir que el código construido que parece ser propenso a problemas (similares a conocidas vulnerabilidades, etc), son errores (bugs) o potenciales fallos de seguridad. La regla básica es: “No estoy al tanto de todos los tipos de explotaciones de seguridad” Debo proteger en contra de lo que conozco y así tengo que ser proactivo”
Otras técnicas
- Uno de los problemas más comunes es uso no verificado de estructuras de tamaño constante y funciones de tamaño de datos dinámicos (el problema del desbordamiento de búfer). Esto es especialmente común en la gestión de cadenas de texto en lenguaje C. Las funciones de biblioteca C como gets nunca deberían ser usadas si el tamaño máximo del búfer de entrada no se pasa como argumento. Las funciones de biblioteca en C como scanf pueden ser usadas seguramente, pero requieren que el programador tenga cuidado con la selección de los formatos seguros de strings, cuidándose antes de usarlos.
- Cifrar/ Autenticar todos los datos importantes transmitido sobre las redes. No intentar implementar un propio esquema de cifrado, pero usar uno ya probado.
- Todos los datos son importantes hasta que se demuestre lo contrario.
- Todos los datos están viciados hasta que se demuestre lo contrario.
- Todo código es inseguro hasta que se demuestre lo contrario.
- Si los datos son probados por correctitud, verificar que están correctos, no que están incorrectos.
- Diseño por contrato: El diseño por contrato usa precondiciones, postcondiciones e invariantes para asegurar que los datos provistos (y el estado del programa como un todo) esta saneado. Esto permite al código documentar todas las hipótesis y hacerlo así seguro. Esto puede envolver la verificación de argumentos a una función o método para validar antes de ejecutar el cuerpo de la función. Después del cuerpo de la función, hacer un chequeo del estado del objeto (en lenguajes de programación orientada a objetos) o guardar los datos y el valor de retorno, antes de que salga de la misma (break/ return/ throw/ error code) es también sabio.
- Aserciones: Entre funciones, se puede querer verificar que se está referenciando algo que no es válido (ej: null) y que el tamaño de los arreglos son válidos antes de referenciar elementos, especialmente instanciando de forma temporal/local. Una buena heurística es no creer en las bibliotecas que se hayan o no se hayan escrito. Cada vez que se llamen estas, verificar lo que se quiere que estas devuelvan. A veces ayuda crear una pequeña biblioteca de funciones de “aserción” y “verificación” para hacerla junto a un logger, para así poder trazar la ruta y reducir la necesidad de extensos ciclos de depuración. Con la llegada de bibliotecas de loggeo y la Programación orientada al Aspecto, muchos de los tediosos aspectos de la programación defensiva son mitigados.
- Preferir el uso de excepciones programables frente a la sobrecarga de los valores de retorno enviando como valores no útiles códigos de error.
Véase también
Enlaces externos
- Jackson, Daniel; Devadas, Srinivas (2001). «Análisis dinámico», en Curso práctico de Ingeniería del Software, capítulo 10. Massachusetts Institute of Technology.
- Creus Solé, Antonio (2005), Fiabilidad y seguridad. Marcombo, págs. 261 y ss. ISBN 8426713629.