Dev.to · 6 min read

5 lições de segurança que aprendi estudando um host temporário de arquivos

5 lições de segurança que aprendi estudando um host temporário de arquivos

Estudei como funciona um projeto open-source de host temporário de arquivos, o Waifuvault, e criei um utilizando Laravel. Neste artigo, vou compartilhar 5 lições de segurança que são essenciais nesse tipo de aplicação e um pouco da experiência de criar esse projeto utilizando Laravel. Não confie no que é enviado pelo usuário Provavelmente você já deve ter ouvido essa frase e nesse tipo de aplicação, é importante fazer a validação dos dados enviados pelo usuário, como nome do arquivo, headers como Content-Type e Content-Length que são variáveis importantes nesse contexto. Dado um cenário em que é enviado um arquivo de 10mb, o servidor precisa verificar o tamanho real recebido para garantir que não ultrapasse o limite permitido pela aplicação. Uma forma de se prevenir isso é durante o recebimento de arquivos por streaming verificar os bytes recebidos conforme for armazenando. Se o limite for atingido, ocorrer uma falha ou divergência, o upload deve ser rejeitado e o arquivo parcial que estava sendo salvo deve ser removido, evitando registros órfãos no sistema. Também é uma boa prática alterar o nome do arquivo para algo gerado pela aplicação, como um UUID. Isso reduz riscos de sobrescrita de arquivos e uso de nomes maliciosos para sua aplicação. flowchart TD A[Usuário envia o arquivo] --> B[Servidor grava os bytes no storage] B --> C{Tamanho gravado éigual ao informado?} C -- Sim --> D[Upload aceito] C -- Não --> E[Upload rejeitado] E --> F[Apaga o arquivo parcialdo storage] Validar o conteúdo real do arquivo, não o que ele diz ser Complementando um pouco o tópico anterior, essa é uma parte interessante, por exemplo, o que pode acontecer é o usuário enviar um arquivo informando uma extensão e Content-Type de .pdf mas o MIME type do conteúdo indica que se trata de um arquivo .png, mais uma divergência das informações e uma possível vulnerabilidade para o sistema. Para identificar o real conteúdo de um arquivo, se é uma foto .png, .jpeg, vídeo .mp4, áudio .mp3, podemos verificar os primeiros bytes do conteúdo. Eles indicam o verdadeiro tipo de arquivo quase como uma assinatura mesmo, então mesmo que você renomeie o arquivo e mude a extensão o conteúdo vai ser o mesmo. Um teste rápido: pegue um arquivo .png qualquer real e use a ferramenta xxd no Linux. Note que os primeiros bytes retornados são 8950 4e47, renomeie esse arquivo para "sexta.pdf" e verifique novamente, os bytes serão os mesmos, é uma curiosidade bem legal. flowchart TD A["Usuário envia shell.png"] --> B[Servidor lê os primeiros bytesdo arquivo] B --> C[Detecta o tipo realpela assinatura] C --> D{Tipo real bate como informado?} D -- Sim --> E[Upload segue o fluxo] D -- Não --> F[Upload rejeitado] Mostrando um pouquinho de código, como o Laravel é um Framework bem completo, é até simples fazer essa validação dentro um FormRequest, segue o exemplo abaixo: use Illuminate\Validation\Validator; public function withValidator(Validator $validator): void { $validator->after(function (Validator $validator) { $file = $this->file('file'); if (! $file || ! $file->isValid()) { return; } $clientMime = $file->getClientMimeType(); $contentMime = $file->getMimeType(); if ($clientMime !== $contentMime) { $validator->errors()->add( 'file', 'O MIME type informado não corresponde ao conteúdo do arquivo.' ); } }); } Convergência entre dois sistemas(Storage e Banco) No primeiro tópico, falei sobre evitar registros órfãos no sistema algo que pode acontecer caso o arquivo seja salvo no storage mas por algum motivo o insert no banco falhar, ou também o contrário disso. Para isso é importante manter o Storage e o Banco de dados em sincronia quanto ao estado de um registro. Acredito que a ordem de salvar primeiro o arquivo no storage e, depois, registrar os metadados no banco de dados seja a mais apropriada, para caso o insert falhe faça a limpeza do arquivo no Storage. flowchart TD A[Salva o arquivo no storage] --> B[Insere o registro no banco] B --> C{Insert funcionou?} C -- Sim --> D[Arquivo e registro em sincronia] C -- Não --> E[Apaga o arquivo do storage] E --> F[Nenhum órfão fica no sistema] Storage privado exige entrega mediada Quando você vai entregar um arquivo ao usuário por um link, isso não é simplesmente entregar a URL do storage para acessar o arquivo diretamente. Isso impossibilitaria a aplicação de regras de acesso, senha e expiração, além de poder expor arquivos. Por isso, o acesso por link deve passar pela a aplicação, para sejam aplicadas essas validações de regras de negócio antes de disponibilizar o arquivo. flowchart LR A[Usuário acessa o link] --> B[Aplicação] B --> C{Regras válidas?expiração, senha, downloads} C -- Não --> D[Responde com erroex.: 404] C -- Sim --> E[(Storage privado)] E --> F[Aplicação devolveo arquivo ao usuário] Expiração não é o mesmo que limpeza Expiração é um estado do arquivo no sistema, o arquivo não é mais disponibilizado pelo servidor, então a tentativa de acesso por link é inválida. Limpeza é a ação de apagar o arquivo do armazenamento e seus metadados do banco de dados. A limpeza não precisa ser feita no momento em que o arquivo expira, ela pode ser feita por um cron job periódico, como a entrega é mediada pela aplicação a tentativa de acesso por link retornaria um erro 404. Logo, um arquivo pode estar expirado, mas ainda existir no armazenamento até que uma ação de limpeza o exclua. Para a limpeza no Laravel, utilizei um Schedule simples diário e a trait Prunable que facilita a exclusão de registros. Vou deixar trechos de código da implementação e seguem os links da documentação para ver mais sobre: pruning-models e schedule. /** * Define a query que seleciona arquivos expirados para remoção. * * @return Builder */ public function prunable(): Builder { return $this->newQuery()->where('expires_at', '

This is a summary aggregated from Dev.to. Read the complete article on the original site:

Read full article at Dev.to

More Programming & Dev News