Skip to main content

Os riscos de setattr: como evitar vulnerabilidades de atribuição em massa no Python

Escrito por
Headshot of Jack Hair

Jack Hair

blog feature pypi spoof

15 de fevereiro de 2023

0 minutos de leitura

A atribuição em massa, também conhecida como autobinding ou injeção de objetos, é uma categoria de vulnerabilidades que ocorre quando a entrada do usuário é vinculada a variáveis ou objetos dentro de um programa.

As vulnerabilidades de atribuição em massa geralmente acontecem quando um invasor adiciona campos inesperados a um objeto para manipular a lógica de um programa. Foi isso que causou uma famosa vulnerabilidade de autenticação do GitHub: a funcionalidade de atribuição em massa do Ruby on Rails permitiu que um invasor adicionasse sua chave pública à organização rails do GitHub e enviasse um commit para a branch master.

Este artigo foi motivado, em parte, pela vulnerabilidade Spring4Shell, uma vulnerabilidade de execução remota de código (RCE) causada pela funcionalidade de autobinding do framework Spring. Isso nos levou a investigar se uma vulnerabilidade semelhante poderia existir no ecossistema Python.

Neste artigo, vamos analisar os riscos potenciais da função setattr() do Python e apresentar formas de evitar vulnerabilidades de atribuição em massa na sua aplicação.

Conheça setattr

Uma forma comum de implementar a atribuição em massa no Python é usar a função integrada setattr(). Ela recebe um objeto, o nome de um atributo e o valor do atributo, e define esse valor no objeto. A função pode adicionar novos atributos e também alterar os existentes.

Embora seja conveniente, permitir que alguém defina o valor de qualquer atributo de um objeto pode ter consequências indesejadas.

Em primeiro lugar, isso abre espaço para as vulnerabilidades tradicionais de lógica relacionadas à atribuição em massa. Campos de objetos que não deveriam ser alterados pelo usuário podem ser modificados.

Em segundo lugar, os atributos que podem ser modificados com setattr não se limitam aos campos definidos pelo programador: também é possível definir funções e atributos integrados, como __class__ e __dict__ . Ao contrário da vulnerabilidade Spring4Shell, setattr não consegue definir atributos aninhados de um objeto, o que limita o alcance de um ataque. No entanto, é fácil provocar uma exceção no momento em que setattr é chamado, definindo __class__ como uma string, ou durante a execução do programa, sobrescrevendo uma função do objeto. Uma exceção provocada dessa forma pode causar um comportamento anormal no programa ou uma negação de serviço.

O código abaixo é um pequeno exemplo que usa setattr para criar um usuário. Se você executá-lo com a entrada first_name=Sam,last_name=S, o 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 fazer o programa falhar devido a uma exceção não tratada, use a entrada first_name=Sam,last_name=S,is_admin=foo. Ela sobrescreve a função is_admin, substituindo-a por uma string e provocando uma exceção quando a função é chamada.

A entrada first_name=Sam,last_name=S,admin=foo sobrescreve o atributo admin do objeto User com a string foo, pois ela é considerada verdadeira. Quando a função is_admin verifica o atributo admin, o usuário é tratado como administrador e o programa exibe Sam S: You are admin!.

Como evitar a atribuição em massa

Uma estratégia geral para reduzir vulnerabilidades de atribuição em massa é manter os objetos que contêm entradas do usuário separados daqueles responsáveis pela lógica interna do programa. Isso pode ser feito criando objetos de transferência de dados (DTOs), que contêm apenas os campos que o usuário pode definir. No Python, isso é simples com dataclasses. O código a seguir mostra como usar um DTO para definir explicitamente os campos permitidos. Se forem especificados campos inválidos, será gerado um TypeError, que poderá ser capturado e tratado para impedir os ataques mencionados acima.

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 a entrada do usuário também é uma forma de evitar vulnerabilidades de atribuição em massa. Como sempre, é melhor validar com base em uma lista de dados aprovados do que verificar se há dados proibidos. O código a seguir ainda usa setattr, mas compara o nome do atributo com uma 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()

O ideal é sempre deixar explícito quais entradas o programa espera do usuário. Mas, se você precisar de mais flexibilidade para lidar com elas, há alternativas à função setattr. O módulo collections da biblioteca padrão do Python oferece duas classes úteis para isso: collections.UserDict e collections.abc.MutableMapping. Você pode herdar dessas classes para adicionar funcionalidades a mapeamentos de chave e valor. Se precisar acessar valores por atributos em vez de chaves, implemente um método __getattr__ na sua classe. No exemplo abaixo, a classe User funciona como um dicionário e pode armazenar qualquer campo fornecido pelo usuário. Ao sobrescrever __getattr__, quando um atributo não é encontrado pelo mecanismo usual de acesso, ele é retornado do dicionário de campos fornecidos pelo usuário. Assim, esses campos podem ser acessados como atributos, mas os atributos da classe não podem ser sobrescritos por entradas do usuário.

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()

Como proteger suas bibliotecas Python

Encontrar o equilíbrio entre segurança, conveniência e flexibilidade pode ser difícil durante o desenvolvimento de software. Algo que parece inofensivo à primeira vista pode expor você e sua aplicação a diversos riscos de segurança — por isso, aprender sobre o assunto e conhecer alternativas é tão importante. Explicamos como setattr funciona, os riscos de usá-lo nas suas bibliotecas Python e várias formas de evitar vulnerabilidades de atribuição em massa. Com esse conhecimento, você está no caminho certo para proteger suas aplicações Python contra ataques e violações de dados. Para saber mais, acesse nossa página inicial e crie uma conta gratuita ou agende uma demonstração para ver como a Snyk pode ajudar a proteger seu código, contêineres, dependências e infraestrutura em nuvem.

Comece a resolver desafios de capture the flag

Aprenda a resolver desafios de capture the flag assistindo sob demanda ao nosso workshop virtual introdutório.