Skip to main content

Los peligros de setattr: cómo evitar vulnerabilidades de asignación masiva en Python

Escrito por
Headshot of Jack Hair

Jack Hair

blog feature pypi spoof

15 de febrero de 2023

0 minutos de lectura

La asignación masiva, también conocida como vinculación automática o inyección de objetos, es una categoría de vulnerabilidades que se producen cuando la entrada del usuario se vincula a variables u objetos dentro de un programa.

Las vulnerabilidades de asignación masiva suelen ser el resultado de que un atacante agregue campos inesperados a un objeto para manipular la lógica de un programa. Esto causó una famosa vulnerabilidad de autenticación de GitHub, en la que la funcionalidad de asignación masiva de Ruby on Rails permitió que un atacante agregara su clave pública a la organización rails de GitHub y enviara un commit a master.

Esta publicación se inspiró, en parte, en la vulnerabilidad Spring4Shell, una vulnerabilidad de ejecución remota de código (RCE) causada por la funcionalidad de vinculación automática incluida en el framework Spring. Esto motivó una investigación para determinar si podía encontrarse una vulnerabilidad similar en el ecosistema de Python.

En esta publicación, analizaremos los posibles riesgos de la función setattr() de Python y veremos cómo prevenir vulnerabilidades de asignación masiva en tu aplicación.

Presentamos setattr

Una forma común de implementar la asignación masiva en Python es usar la función integrada setattr(). Esta función recibe un objeto, el nombre de un atributo y su valor, y asigna ese valor al atributo del objeto. Puede agregar atributos nuevos y modificar los existentes.

Aunque es práctico, permitir que un usuario establezca el valor de cualquier atributo de un objeto puede tener consecuencias imprevistas.

En primer lugar, puede dar lugar a las vulnerabilidades tradicionales de lógica por asignación masiva en un programa. Ahora se pueden modificar campos de un objeto que no estaban destinados a ser modificados por un usuario.

En segundo lugar, los atributos que se pueden modificar con setattr no se limitan a los campos definidos por el programador: también se pueden establecer funciones y atributos integrados como __class__ y __dict__ . A diferencia de la vulnerabilidad Spring4Shell, setattr no puede establecer atributos secundarios anidados de un objeto, lo que limita el alcance de un ataque. Sin embargo, es fácil provocar una excepción cuando se llama a setattr, asignando un valor de cadena a __class__, o más adelante durante la ejecución del programa, sobrescribiendo una función del objeto. Provocar una excepción de esta manera puede dar lugar a un comportamiento anormal del programa o a una denegación de servicio.

El código de abajo es un pequeño ejemplo que usa setattr para crear un usuario. Si se ejecuta con la entrada first_name=Sam,last_name=S, el resultado será Sam S: Permission denied.

class User():
	def __init__(self, admin=False):
    		self.admin = admin

	def is_admin(self):
    		return "You are admin!" if self.admin else "Permission denied"

def parse_input(usr_in):
	return dict([tuple(kv.split("=", 1)) for kv in usr_in.split(",") if "=" in kv])

def create_user(info):
	user = User()
	for key, value in info.items():
    	setattr(user, key, value)
	return user

def main():
	user = create_user(parse_input(str(input())))
	if hasattr(user, "first_name") and hasattr(user, "last_name"):
    		print(f"{user.first_name} {user.last_name}: {user.is_admin()}")

if __name__ == "__main__":
	main()

Para hacer que el programa falle debido a una excepción no controlada, se puede usar la entrada first_name=Sam,last_name=S,is_admin=foo. Esta sobrescribirá la función is_admin y la reemplazará por una cadena, lo que provocará una excepción cuando se llame a la función.

La entrada first_name=Sam,last_name=S,admin=foo sobrescribe el atributo admin del objeto User con la cadena foo, porque este es un valor verdadero (Truthy). Cuando la función is_admin comprueba el atributo admin, el usuario se trata como administrador y se muestra Sam S: You are admin!.

Cómo prevenir la asignación masiva

Una estrategia general para mitigar las vulnerabilidades de asignación masiva consiste en mantener separados los objetos que contienen entradas del usuario de los objetos responsables de la lógica interna del programa. Para ello, se crean objetos de transferencia de datos (DTO), que solo contienen los campos que un usuario puede establecer. En Python, esto es fácil de hacer con dataclasses. El siguiente código muestra cómo se puede usar un DTO para definir explícitamente los campos permitidos. Si se especifican campos incorrectos, se genera un TypeError, que luego se captura y se maneja para prevenir los ataques mencionados.

from dataclasses import dataclass

@dataclass
class UserDTO():
	first_name: str
	last_name: str

class User():
	def __init__(self, dto, admin=False):
    		self.first_name = dto.first_name
    		self.last_name = dto.last_name
    		self.admin = admin

	def is_admin(self):
    		return "You are admin!" if self.admin else "Permission denied"

def parse_input(usr_in):
	return dict([tuple(kv.split("=", 1)) for kv in usr_in.split(",") if "=" in kv])

def create_user(info):
	try:
    		return User(UserDTO(**info))
	except TypeError:
    		return None

def main():
	user = create_user(parse_input(str(input())))
	if user:
    		print(f"{user.first_name} {user.last_name}: {user.is_admin()}")

if __name__ == "__main__":
	main()

Validar la entrada del usuario también es una forma de prevenir las vulnerabilidades de asignación masiva. Como siempre, es mejor validar con una lista de datos aprobados en lugar de buscar datos prohibidos. El siguiente código sigue usando setattr, pero compara el nombre del atributo con una lista de atributos permitidos.

class User():
	allowed_fields = ["first_name", "last_name"]

	def __init__(self, admin=False):
    		self.admin = admin

	def is_admin(self):
    		return "You are admin!" if self.admin else "Permission denied"

def parse_input(usr_in):
	return dict([tuple(kv.split("=", 1)) for kv in usr_in.split(",") if "=" in kv])

def create_user(info):
	user = User()
	for key, value in info.items():
    		if key in User.allowed_fields:
        			setattr(user, key, value)
    		else:
        			return None
	return user

def main():
	user = create_user(parse_input(str(input())))
	if user:
    		print(f"{user.first_name} {user.last_name}: {user.is_admin()}")

if __name__ == "__main__":
	main()

Siempre es mejor especificar explícitamente qué entradas del usuario espera un programa, pero si necesitas más flexibilidad para manejarlas, existen alternativas a la función setattr. El módulo collections de la biblioteca estándar de Python ofrece dos clases útiles para ayudarte: collections.UserDict y collections.abc.MutableMapping. Se puede heredar de estas clases para agregar funcionalidad a las asignaciones de clave y valor. Si necesitas acceder a los valores mediante atributos en lugar de claves, puedes hacerlo implementando un método __getattr__ en tu clase. En el siguiente ejemplo, la clase User funciona como un diccionario y puede almacenar cualquier campo proporcionado por el usuario. Al sobrescribir __getattr__, cuando se accede a un atributo que no se encuentra mediante el mecanismo habitual de acceso a atributos, este se devuelve desde el diccionario de campos proporcionados por el usuario. Así, se puede acceder a estos campos de la misma manera que a los atributos, pero se evita que la entrada del usuario sobrescriba los atributos de la clase.

from collections import UserDict

class User(UserDict):
	def __init__(self, info, admin=False):
    		self.admin = admin
    		super().__init__(info)

	def is_admin(self):
    		return "You are admin!" if self.admin else "Permission denied"

	def __getattr__(self, name):
    	if name in self:
     		   	return self[name]
    		else:
        		raise AttributeError("'User' object has no attribute '{}'".format(name))

def parse_input(usr_in):
	return dict([tuple(kv.split("=", 1)) for kv in usr_in.split(",") if "=" in kv])

def create_user(info):
	return User(info)

def main():
	user = create_user(parse_input(str(input())))

	if "first_name" in user and "last_name" in user:
    		print(f"{user.first_name} {user.last_name}: {user.is_admin()}")

if __name__ == "__main__":
	main()

Protege tus bibliotecas de Python

En pleno desarrollo de software, puede ser difícil encontrar el equilibrio entre la seguridad, la comodidad y la flexibilidad. Algo que parece inofensivo a primera vista puede exponerte a ti y a tu aplicación a diversos riesgos de seguridad; por eso son tan importantes la educación y los métodos alternativos. Hemos explicado cómo funciona setattr y los peligros de depender de esta función en tus bibliotecas de Python, además de varias formas de prevenir vulnerabilidades de asignación masiva. Con estos conocimientos, estás más cerca de proteger tus aplicaciones de Python contra ataques y filtraciones de datos. Para obtener más información, visita nuestra página de inicio y crea una cuenta gratuita, o agenda una demostración para ver cómo Snyk puede ayudar a proteger tu código, contenedores, dependencias e infraestructura en la nube.

Empieza con los desafíos de Capture the Flag

Aprende a resolver desafíos de captura la bandera con nuestro taller virtual introductorio a pedido.

Leer más

illustration hero ai
Blog

PHP corrige dos vulnerabilidades críticas de RCE sin autenticación (CVE-2026-109618, CVE-2026-109619): actualiza ahora

Dos fallas críticas en el ciclo de solicitudes de PHP pueden permitir la ejecución remota de código sin autenticación. Actualiza a una versión corregida o instala una corrección retroportada por un proveedor para las versiones anteriores.

blog feature ai lilac
Blog

Convierte tu backlog de vulnerabilidades en pull requests con la remediación agéntica

Evo Remediation Agent ya está disponible en Open Preview e incorpora la resolución automatizada de dependencias a Evo Agentic AppSec. Ahora, los equipos de AppSec pueden ejecutar campañas de remediación continuas

AI Background min crop
Blog

La IA activa todos los riesgos que has aceptado: siete ideas del panel de seguridad de IA sin filtros

La IA hace que los ataques sean más baratos y rápidos, y deja al descubierto riesgos que las organizaciones ya enfrentan. Cuatro líderes de seguridad explican por qué la defensa a velocidad de máquina y la corrección más rápida son esenciales.