Arquivo para setembro 2010
Nunca retorne ponteiros para atributos de uma classe nem deixem atributos com acesso ‘public’
Esse post surgiu de uma dúvida que tive:
Como será que é feita a “proteção” dos atributos e funções-membro de classes em C++?
Com isso resolvi fazer alguns testes e eis a classe de teste:
#include <iostream>
#include <string>
class security
{
private:
std::string name;
std::string password;
int id;
/* more stuff */
public:
security() : name("John"), password("s3cr3t"), id(10) {}
std::string* name_ptr() { return &name; }
void print_pass()
{
std::cout << "The current password is: " << password << std::endl;
}
};
A pergunta é: aonde pode estar o perigo nesse código (obviamente além de ter uma função-membro que imprime a senha na tela)?
Onde mora o perigo
O perigo maior reside na função-membro name_ptr(). Essa função por algum motivo (talvez o desenvolvedor queria fazer de name_ptr() um getter/setter por exemplo) delega um ponteiro para o atributo name.
Por ela retornar um ponteiro para um atributo da classe, acabamos com o sistema de proteção de acesso a atributos/funções-membro do C++:
int main()
{
security obj;
std::string* name = obj.name_ptr();
std::cout << "Name: " << *name << std::endl;
std::string* password = (std::string*)(name + 1);
std::cout << "Password was: " << *password << std::endl;
// Changing password
*password = "l33t";
obj.print_pass(); // whoa!!
// For other data types
int* id = (int*)(password + 1);
std::cout << "ID: " << *id << std::endl;
return 0;
}
Ou seja, a partir do ponteiro retornado por obj.name_ptr(), através de aritmética de ponteiros[0], podemos acessar qualquer outro atributo de um objeto da classe security, mesmo que estes não sejam public. Como podemos ver no exemplo acima, a partir do atributo name, podemos facilmente obter acesso aos outros atributos.
Outra falha é quando deixamos algum atributo como public:
#include <iostream>
#include <string>
class security
{
public:
std::string name;
private:
std::string password;
int id;
/* more stuff */
public:
security() : name("John"), password("s3cr3t"), id(10) {}
void print_pass()
{
std::cout << "The current password is: " << password << std::endl;
}
};
Dessa maneira podemos também obter o endereço de name, agora apenas usando o operador & unário:
int main()
{
security obj;
std::string* name = &(obj.name);
std::cout << "Name: " << *name << std::endl;
std::string* password = (std::string*)(name + 1);
std::cout << "Password was: " << *password << std::endl;
// Changing password
*password = "l33t";
obj.print_pass(); // whoa!!
// For other data types
int* id = (int*)(password + 1);
std::cout << "ID: " << *id << std::endl;
return 0;
}
Observem que na linha 5, peguei o endereço do atributo name e fiz a mesma coisa do exemplo anterior.
Para os dois exemplos a saída é a mesma:
Name: John Password was: s3cr3t The current password is: l33t ID: 10
Voltando à minha dúvida inicial…
Conclusão: pelo menos no compilador g++, a proteção é feita apenas pelo compilador (compile-time) e nenhum outro tipo de impedimento é usado. Ou seja, se você “adivinhar” o endereço de memória de um atributo de um objeto, você poderá ter acesso total à ele e aos outros atributos desse objeto.
Então, na hora de desenvolvermos nossas classes em C++, nada de retornar ponteiros para atributos de um objeto, nem de deixar atributos public quando se tem outros atributos não-public.
Update
Logo depois de eu fazer esse post, o WP automaticamente postou em meu Twitter e Facebook o link para este post. No Facebook recebi um comentário de um excelente programador o Maurício Collares (ou MauricioC no TopCoder[1]). Acho pertinente colar o comentário e também minha reposta aqui uma vez que estou vendo que esse post está tendo uma grande quantidade de leituras.
Maurício Collares Neto: Não sei se entendi qual o problema de alguém que já tem acesso de leitura à posicao “secreta” de memória (no sentido de não tomar um SIGSEGV) acessar tal posicao. Private e public são anotacoes existentes por motivos de Engenharia de Software, para evitar quebra de abstracoes; a grande maioria das linguagens oferece um jeito “fácil” de quebrar tal abstracao caso você precise fazê-lo (vide instrospeccao em Java).
Além disso, se você tem o endereco da da instäncia da classe (que pode ser obtido mesmo que vocë náo tenha nenhum membro público), você já tem acesso ao endereco de memória de todas as outras variáveis, do mesmo jeito; membros de classes são só syntactic sugar pra aritmética de ponteiros.
Murilo Adriano Vasconcelos: Interessante, eu não tinha pensado no fato de através do endereço do objeto podemos facilmente acessar qualquer atributo do objeto. E pensando melhor, mesmo que não houvesse esse tipo de facilidade no ponto de vista de objetos, teríamos acesso de alguma forma no no ponto de vista de processos, já que os processos tem total acesso às àreas de memória alocadas por ele.
Assim, e com o que você disse, fica totalmente anula o sentido do título do post (já que, por exemplo, seria o mesmo que dizer “Nunca instancie objetos”, um absurdo), porém poderei fazer uma atualização no post no sentido de apenas expor essa “feature” adicionando essa idéia de através de uma instância da classe acessar os outros atributos.
De qualquer forma, valeu pelo comentário, pena que foi só aqui no FB e não no blog, onde mais pessoas poderiam ter acesso à essa conversa.
Referências
[0] - Aritmética de ponteiros em C e C++ (Wikipedia)
[1] – TopCoder