Comprobación de vulnerabilidades en su código Python con Bandit

En este artículo, explorará cómo usar Bandit, un analizador de seguridad de código abierto para proyectos de Python. ¡Bandits genera informes sobre vulnerabilidades en nuestro código!

Introducción

Como desarrolladores, se nos alienta desde el comienzo del viaje a escribir código limpio. Igual de importante, pero de lo que se habla menos, es escribir y usar código seguro.

En los proyectos de Python, normalmente instalamos módulos y paquetes de terceros para evitar desarrollar soluciones que ya existen. Sin embargo, esta práctica común es la razón por la que los piratas informáticos explotan las dependencias para causar estragos en nuestro software y por la que necesitamos poder detectar cuando algo anda mal. Como tal, usamos herramientas como Bandido, una utilidad de análisis de seguridad de código abierto para proyectos de Python.

En esta guía, exploraremos cómo las líneas de código simples pueden terminar siendo destructivas y cómo podemos usar Bandit para ayudarnos a identificarlas.

Vulnerabilidades de seguridad en Python

Una vulnerabilidad de seguridad en nuestro código es una falla que los agentes malintencionados pueden aprovechar para explotar nuestros sistemas y/o datos. Mientras programa en Python, podría haber un uso vulnerable de llamadas funcionales o importaciones de módulos que pueden ser seguros cuando se invocan localmente, pero podrían abrir puertas para que usuarios malintencionados manipulen el sistema cuando se implementan sin las configuraciones correctas.

Probablemente te hayas encontrado con varios de estos en tus actividades diarias de codificación. Algunos de los ataques y exploits más comunes son abordados en gran medida por marcos y sistemas modernos que anticipan tales ataques.

Aquí hay algunos:

  • Inyección de comandos del sistema operativo: basado en el humilde módulo subproceso que utiliza para ejecutar utilidades de línea de comandos e invocar procesos relacionados con el sistema operativo. El siguiente fragmento utiliza el módulo subproceso para realizar una búsqueda de DNS y devuelve el resultado:
1
2
3
4
5
# nslookup.py
import subprocess
domain = input("Enter the Domain: ")
output = subprocess.check_output(f"nslookup {domain}", shell=True, encoding='UTF-8')
print(output)

¿Qué podría salir mal aquí?

En un escenario ideal, el usuario final proporcionará un DNS y el script devolverá los resultados del comando nslookup. Pero, si proporcionaran un comando basado en el sistema operativo como ls junto con el DNS, se recibiría el siguiente resultado: el comando también se ejecutaría:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
$ python3 nslookup.py
Enter the Domain: wikihtp.com ; ls
Server:         218.248.112.65
Address:        218.248.112.65#53

Non-authoritative answer:
Name:   wikihtp.com
Address: 172.67.136.166
Name:   wikihtp.com
Address: 104.21.62.141
Name:   wikihtp.com
Address: 2606:4700:3034::ac43:88a6
Name:   wikihtp.com
Address: 2606:4700:3036::6815:3e8d

config.yml
nslookup.py

Al permitir que alguien pase una parte de un comando, les permitimos acceder a la terminal del nivel del sistema operativo.

Imagínese cuán destructivas podrían ser las cosas si el actor malicioso proporcionara un comando como cat /etc/passwd que revelaría las contraseñas de los usuarios existentes. Tan simple como suena, el módulo subproceso puede ser muy arriesgado de usar.

  • Inyección SQL: los ataques de inyección SQL son raros en estos días, gracias a las funcionalidades ORM que se utilizan ampliamente. Pero si todavía está alineado con el uso de SQL sin procesar, debe saber cómo se construyen sus consultas SQL y qué tan seguros se validan y pasan sus parámetros de consulta.

Considere el siguiente fragmento:

1
2
3
4
5
6
7
from django.db import connection

def find_user(username):
    with connection.cursor() as cur:
        cur.execute(f"""select username from USERS where name = '%s'""" % username)
        output = cur.fetchone()
    return output

La llamada a la función es simple: pasa una cadena como argumento, diga "Foobar" y la cadena se inserta en la consulta SQL, lo que da como resultado:

1
select username from USERS where name = 'Foobar'

Sin embargo, al igual que en el problema anterior, si alguien agregara un carácter ;, podría encadenar varios comandos. Por ejemplo, insertando '; DESCARGAR USUARIOS DE LA TABLA; -- resultaría en:

1
select username from USERS where name = ''; DROP TABLE USERS; --'

La primera instrucción se ejecutaría justo antes de que la base de datos elimine toda la tabla USUARIOS. ¡Ay!

Observe cómo se ha comentado la última cita con guiones dobles. Los parámetros de consulta SQL pueden convertirse en pesadillas, si no se revisan adecuadamente. Aquí es donde las herramientas de seguridad pueden ayudar a detectar líneas de código no intencionales pero dañinas.

Bandido

Bandit es una herramienta de código abierto escrita en Python que lo ayuda a analizar su código Python y encontrar problemas de seguridad comunes en él. Podrá escanear su código Python, detectar vulnerabilidades y exploits como los que se mencionaron en la sección anterior. Bandit se puede instalar localmente o dentro de su entorno virtual fácilmente a través de pip:

1
$ pip install bandit

Bandit se puede utilizar desde las siguientes perspectivas:

  • DevSecOps: incluye Bandit como parte de las prácticas de integración continua (CI).
  • Desarrollo: Bandit se puede usar localmente como parte de la configuración de desarrollo local, donde los desarrolladores pueden tener control sobre la explotación de funciones antes de confirmar el código.

Usando Bandit

Bandit se puede integrar fácilmente como parte de las pruebas de CI y se pueden realizar comprobaciones de vulnerabilidad comunes antes de enviar el código a producción. Por ejemplo, los ingenieros de DevSecOps pueden invocar Bandit cada vez que se genera una solicitud de extracción o se confirma un código, para mejorar la seguridad. Según las pautas de la organización, los módulos de importación y las llamadas a funciones se pueden permitir o restringir.

Bandit proporciona control a los usuarios sobre qué módulos usar y qué módulos poner en la lista negra. Este control se define dentro del archivo de configuración, que se puede generar utilizando la herramienta bandit-config-generator. El resultado de las pruebas de código que se ejecutan se puede exportar en forma de CSV, JSON, etc.

El archivo de configuración se puede generar como:

1
$ bandit-config-generator -o config.yml

El archivo config.yml generado contiene varias partes correspondientes a las pruebas que se pueden permitir o revocar, las llamadas a funciones que se pueden permitir o revocar, junto con la longitud máxima de las claves criptográficas. El usuario puede usar bandit especificando este archivo de configuración o realizar todas las pruebas simplemente pasando el directorio del proyecto:

1
2
3
4
5
6
7
8
$  bandit -r code/ -f csv -o out.csv
[main]  INFO    profile include tests: None
[main]  INFO    profile exclude tests: None
[main]  INFO    cli include tests: None
[main]  INFO    cli exclude tests: None
[main]  INFO    running on Python 3.8.5
434 [0.. 50.. 100.. 150.. 200.. 250.. 300.. 350.. 400.. ]
[csv]   INFO    CSV output written to file: out.csv

En esta llamada de Bandit, especificará el directorio del proyecto usando el indicador -r y escribirá la salida como un CSV usando el indicador -o. Bandit prueba todos los scripts de python dentro de este directorio de proyecto y devuelve el resultado como un CSV. La salida es muy detallada y así es como se ve:

Fig 1. Bandit Output

Como se mencionó en la sección anterior, la importación del módulo subprocess y el argumento shell=True son una amenaza de alta seguridad. Si es inevitable usar este módulo y argumento, estos pueden incluirse en la lista blanca en el archivo de configuración y hacer que se salten las pruebas al incluir los códigos B602 (subprocess_popen_with_shell_equals_true) y B404 (import_subprocess) en "skips\ “. Puede encontrar estos códigos en el archivo de configuración generado. Las pruebas que se incluyen en el archivo en la sección skips como:

1
skips: [B602, B404]

Si vuelve a ejecutar las pruebas de Bandit usando el archivo de configuración generado, esto dará como resultado un archivo CSV vacío que indica que se pasaron todas las pruebas:

1
2
3
4
5
6
7
8
9
> bandit -c code/config.yml -r code/ -f csv -o out2.csv
[main]  INFO    profile include tests: None
[main]  INFO    profile exclude tests: B404,B602
[main]  INFO    cli include tests: None
[main]  INFO    cli exclude tests: None
[main]  INFO    using config: code/config.yml
[main]  INFO    running on Python 3.8.5
434 [0.. 50.. 100.. 150.. 200.. 250.. 300.. 350.. 400.. ]
[csv]   INFO    CSV output written to file: out2.csv

Para las colaboraciones dentro de una organización, este archivo de configuración de Bandit debe integrarse en proyectos recién creados para que los desarrolladores puedan tener acceso a él incluso localmente.

Conclusión

El código debe estar limpio y seguro. En esta breve guía, echamos un vistazo a Bandit, una biblioteca de Python utilizada para identificar problemas de seguridad comunes con módulos que probablemente ya estés usando. o.