Postado em 15 de dezembro de 2008
Forçar o download de um arquivo com PHP
Atenção! Essa postagem foi escrita há mais de 2 anos. Na informática tudo evolui muito rápido e algumas informações podem estar desatualizadas. Embora o conteúdo possa continuar relevante, lembre-se de levar em conta a data de publicação enquanto estiver lendo. Caso tenha sugestões para atualizá-la, não deixe de comentar!
Prefácio
Há alguns posts atrás eu sugeri uma rotina para forçar o download de arquivos, porém uma delas tem um sério problema se não for implementada com uma verificação de segurança.
Confesso que eu não havia percebido este equívoco e agradeço ao leitor Luiz F. G. Deitos por ter me questionado acerca desta vulnerabilidade no script.
Atualização em 27/01/2016: Modifiquei algumas coisas no script. Deixei ele mais simples e mais inteligente. Obviamente, continuo aceitando críticas e sugestões. Obrigado a todos pela colaboração!
Forçando o download sem PHP
Antes de ensinar a forçar o download com PHP, gostaria de deixar uma dica que você pode aplicar utilizando apenas HTML. Dependendo do seu caso, pode ser uma solução mais simples e que funcionará da mesma forma.
Sugiro que utilize quando você tem o arquivo físico e gostaria apenas que a pessoa baixasse ele, sem utilizar nenhuma rotina de proteção, ou seja, apenas precisa que a pessoa clique e que o navegador baixe o arquivo em vez de exibir.
Existe um atributo HTML chamado download. Com ele você pode indicar em um link (<a>) que você quer que o navegador baixe aquele arquivo.
<a href="/caminho/para/arquivo.jpg" download>
Esse método não é suportado pelo Safari e por versões mais antigas do Internet Explorer, pois é um atributo do HTML5. Você pode ler mais sobre o funcionamento e variações dessa técnica no site W3schools.
Conceito
Caso a opção HTML não resolva seu problema, vamos desenvolver o script PHP! Devemos tomar cuidado com uma vulnerabilidade que se baseia no fato de passar um caminho de arquivo malicioso para o script, fazendo com que sejam baixados outros arquivos, que não os que o programador especifica.
Muito cuidado agora: O que pode acontecer se for requisitada a página index.php, ou até mesmo algo como “../conexao.php”? Você deve estar ciente desses detalhes antes de colocar a rotina para rodar.
Como os dados vêm crus, você tem acesso a todos os includes, podendo assim fazer a varredura dos arquivos que são requisitados e também os seus respectivos caminhos, podendo depois baixá-los.
Por isso, antes de disponibilizar os arquivos, vamos realizar algumas filtragens nos valores que chegam do navegador.
Criando a rotina de download
O que temos de fazer é bastante simples. Filtrar a string de entrada para remover qualquer retorno de diretório do tipo ./ (diretório atual) e ../ (diretório acima), ou seja, devemos ficar apenas com o nome do arquivo.
Ao recebermos o arquivo via get (download.php?arquivo=baixar.jpg), vamos coloca-lo em $arquivo.
Em seguida filtramos a string, os retornos de diretório e, em seguida, se o arquivo realmente existe (file_exists).
Se tudo correr bem, enviamos um header indicando que o arquivo deverá ser baixado (octet/stream, ou seja, um executável).
O Content-disposition irá indicar que é um arquivo do tipo anexo e qual é o seu nome. Já o Content-length indica o tamanho do arquivo.
readfile(), por fim, envia o arquivo para o navegador.
Qualquer cabeçalho que seja enviado a mais irá causar um bug em navegadores mais antigos, no qual não é mostrado o nome do arquivo na hora do download.
Para evitar que o fulano tente baixar arquivos de nosso diretório atual, vamos colocar todos os downloads em outro diretório. Para tanto, criaremos uma constante chamada DIR_DOWNLOAD e vamos definir um diretório para os arquivos.
Importante: Neste diretório coloque APENAS arquivos de download, para evitar que o usuário baixe arquivos que não deve. Você também pode adaptar a rotina para renomear o arquivo na hora do download ou para buscar o caminho real do arquivo no banco. Use a criatividade.
Obviamente a rotina é bastante simples. Cabe a você refiná-la e adaptá-la às suas necessidades.
<?php // Aqui vale qualquer coisa, desde que seja um diretório seguro :) define('DIR_DOWNLOAD', '../downloads/'); // Vou dividir em passos a criação da variável $arquivo pra ficar mais fácil de entender, mas você pode juntar tudo $arquivo = $_GET['arquivo']; // Retira caracteres especiais $arquivo = filter_var($arquivo, FILTER_SANITIZE_STRING); // Retira qualquer ocorrência de retorno de diretório que possa existir, deixando apenas o nome do arquivo $arquivo = basename($arquivo); // Aqui a gente só junta o diretório com o nome do arquivo $caminho_download = DIR_DOWNLOAD . $arquivo; // Verificação da existência do arquivo if (!file_exists($caminho_download)) die('Arquivo não existe!'); header('Content-type: octet/stream'); // Indica o nome do arquivo como será "baixado". Você pode modificar e colocar qualquer nome de arquivo header('Content-disposition: attachment; filename="'.$arquivo.'";'); // Indica ao navegador qual é o tamanho do arquivo header('Content-Length: '.filesize($caminho_download)); // Busca todo o arquivo e joga o seu conteúdo para que possa ser baixado readfile($caminho_download); exit;
Um exemplo de link para baixar a imagem exemplo.jpg ficaria assim:
<a href="download.php?arquivo=exemplo.jpg">Baixar a imagem</a>
Conclusão
Agora temos um script mais seguro e mais bacana!
Agradeço a todos que têm entrado e comentado, inclusive fazendo críticas sobre o código e sugerindo melhoras.
Um grande abraço a todos e fiquem com Deus!
Rafael Jaques