Como implementar o upload de ficheiros para um servidor da forma correta: questões de segurança e escalabilidade
Quem já usou redes sociais, já deve ter reparado nas fotos de perfil. Normalmente, existe uma função que permite mudar essa foto de perfil, atualizar a foto de perfil e até mesmo apagar a foto de perfil. O Facebook, o Instagram, o WhatsApp tem essas funcionalidades.
A questão é que criar a funcionalidade de upload de imagens é simples, mas fazer a funcionalidade de forma correta é um bocado mais complicado e existem coisas que devem ser pensadas. Como utilizador, quero mudar a minha foto de perfil. Mas também como utilizador, quero que a minha foto de perfil esteja em segurança.
A verdade é que a primeira coisa que temos de assumir é que uma vez na Internet para sempre na Internet. É possível resgatar fotos apagadas só que isto já é outro assunto para outro artigo.
É possível pesquisar a foto de perfil do LinkedIn no Google Images. Se pesquisarem o vosso nome completo no Google Images, a foto de perfil deverá aparecer em algum lugar. Isso são os chamados ficheiros estáticos. Para armazenar esses ficheiros estáticos, podemos usar CDN’s (Content Delivery Networks). Estas redes são distribuídas geograficamente e permitem acesso rápido e seguro a conteúdos como vídeos, imagens, páginas estáticas, ficheiros de código, documentos (Cloudflare, 2023) …
Desta forma garante-se a segurança dos dados que é o caso que acontece com a AWS, a Cloudflare e entre outros. No entanto há ainda outras coisas que podem ser melhoradas.
Há uns meses, assisti a uma palestra de um engenheiro da Bosch na minha universidade que a sua função era Ethical Hacker. Recomendo aos leitores do blogue, assistirem a esta palestra que vai exatamente de encontro ao assunto que este artigo aborda. Isto é, com técnicas de OSINT, é possível aceder a dados desprotegidos (IPCA, 2023)
Palestra do engenheiro da Bosch: https://www.youtube.com/watch?v=SztXU5ZMDJo
Então, como é que protegemos o acesso a estes recursos estáticos?
Pode-se usar as CDN’s, mas também é preciso garantir que o servidor que está a ser desenvolvido seja pensado para a segurança de dados.
Primeiro ponto importante, deve-se ter um sistema de autenticação seguro. Pode ser usado tokens de sessão temporários e de pouca duração para obrigar o utilizador a se autenticar novamente para ter acesso ao recurso.
Em segundo lugar, deve-se garantir a integridade do ficheiro que é carregado para o servidor através da comparação das hashes do ficheiro de origem com o ficheiro carregado. Ao enviar o pedido para o servidor tudo pode acontecer no meio. Se o ficheiro for modificado, significa que alguém intercetou o ficheiro e adicionou código malicioso e esse ficheiro deve ser imediatamente apagado do servidor.
Em terceiro lugar, no caso das fotos de perfil de um utilizador, se outro utilizador conhecer o servidor de ficheiros e o nome de outro utilizador, pode buscar facilmente a foto do outro utilizador. Isso pode ser uma falha de segurança. O que se pode fazer neste caso é “cifrar” o nome de utilizador e colocar a “cifra” no nome do ficheiro. É mais difícil aceder a esses ficheiros. Para melhorar, pode-se criar uma outra rota para descarregar o ficheiro. No entanto, perde-se na escalabilidade do servidor. A rota não seria para descarregar, mas sim seria para retornar uma URI assinada para outro servidor e a aplicação é que vai buscar a foto ao servidor de ficheiros. Mas aqui o backend vai buscar apenas a foto daquele utilizador e só o backend conhece o servidor de ficheiros.
Ou seja, isto funcionará com dois servidores.
Existe diversas formas de garantir a segurança. Agora, será demonstrada a implementação disto com o exemplo das fotos de perfil.
No pseudocódigo seguinte encontra-se representado como é que funcionará o upload:
Neste exemplo espera-se que exista um serviço de
autenticação registo e login. Implementou-se o serviço de autenticação e login
que não será abordado aqui.
Os códigos seguintes foram feitos em Node.js, mas a lógica é replicável em qualquer runtime (.NET Core, Flask, Django…)
O que se fez primeiro foi instalar um pacote npm chamado “multer”.
Este pacote lida com formulários multipart/form-data. Estes formulários permitem
o upload de ficheiros. Podia-se usar JSON, mas teria de se colocar no corpo o
código Base64 do ficheiro e isso é pesado e não vale a pena estar a criar
complicação desnecessária.
Após a configuração da autenticação, criou-se um ficheiro de
configuração para o multer:
Na constante storage é definido a pasta de destino “./uploads”
No filename é verificado se o utilizador está autenticado e
gerado a “cifra do ficheiro”. Para este caso usou-se uuids porque ao usar bcrypt,
os nomes de ficheiro ficam com caracteres especiais e isso baralha totalmente o
sistema operativo (seja Windows, Linux ou MacOS).
Depois é criado o middleware express para o multer em linha
18.
O fileFilter é uma função que permite limitar as extensões
de ficheiro:
No caso serão verificados os mimetypes de cada ficheiro o que é correto, porque uma imagem pode ser .html e ser do mimetype image/png e sendo que uma extensão pode ter vários mimetypes. O .jpg pode ter o mimetype image/jpg e image/jpeg
De seguida, foi criado um gerador de checksums:
Nesta função é criado o checksum de um ficheiro existente no
servidor
Agora falta implementar a funcionalidade no servidor. Na imagem seguinte encontra-se a implementação da mudança de foto.
É verificada se o utilizador está autenticado. Se estiver, ele procura o utilizador na base de dados. Se existir, o upload pode ser realizado.
Em multerMiddleware é verificado a existência de algum erro
após o upload. Se houver erro, devolve 400. Senão, o ficheiro é renomeado e criado
o checksum.
Tendo sido gerado o checksum, é verificado se os checksums do
ficheiro do cliente e do servidor são os mesmos:
Isto significa que o checksum do ficheiro final não é o
mesmo simulando quando um hacker modifica o ficheiro.
No caso de sucesso, se agora copiarmos o caminho do ficheiro
e apontarmos esse caminho no servidor, teremos acesso ao ficheiro:
Como é possível verificar temos acesso ao recurso pretendido.
No entanto esta lógica é simples e para melhorar este pequeno programa o mais
interessante seria guardar esse ficheiro num servidor externo de ficheiros,
isto é, teríamos de passar este caminho num request axios para o servidor pretendido
e enviar esse ficheiro para esse servidor. No entanto para este exemplo, foi
decidido não complicar, mas para a questão da escalabilidade, estes ficheiros estariam
num servidor Apache ou Nginx ou um servidor HTTP ou um servidor FTP. Desta
forma, melhora-se a escalabilidade do sistema, pois armazenar esses ficheiros
no servidor torna-o pesado.
Portanto a arquitetura escalável deste sistema seria esta:
Este artigo teve o propósito de abordar as questões da
segurança da informação em upload de ficheiros. Desta forma é mais difícil haver
falhas de segurança no sistema, garantindo a autenticação, a integridade dos
ficheiros enviados e a maior dificuldade em aceder aos ficheiros de outros. É demonstrado
que a arquitetura implementada não é escalável e além disso acaba por não ser
segura. Na imagem anterior encontra-se uma forma mais segura e escalável de carregar
ficheiros para um servidor. A segurança nos sistemas de informação torna-se um
fator importante e da forma que é demonstrada na imagem anterior, é mais seguro
evitando que quando alguém com conhecimentos OSINT (Open Source Intelligence)
vasculhe ficheiros confidenciais, tal como poderão ver na palestra. Cuidado
com isto. E mais importante, nenhum sistema é totalmente seguro e cabe aos
programadores de servidores terem noções de segurança, como não guardar passwords
no código, as passwords devem ser cifradas e guardadas na base de dados,
acessos a servidores e bases de dados devem estar em variáveis de ambiente, uploads
de ficheiros devem ser seguros desta ou usando CDN's como referido no início.
Referencias:
Cloudflare. (2023). What is a CDN? | How do CDNs work? | Cloudflare. Cloudflare.
https://www.cloudflare.com/learning/cdn/what-is-a-cdn/