Forçar o download de um arquivo com PHP

Download

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