Forçar o download de um arquivo com PHP

Download

Forçar o download de um arquivo com PHP

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

  • http://www.souweb.info Carlos Tristacci

    Grande Jaques!

    Show o script.

    Acabo de implementá-lo numa aplicação.

    Abraços!

  • http://www.phpit.com.br Rafael Jaques

    Eu dei uma melhorada nesse script…

    Espero que tenha ficado bom :)

    • Fabiano

      Olá Rafael! estou com um problema em meu script de download. Ele está corrompendo arquivos .exe. Testei direto pelo link do servidor e o arquivo funcionou, mas se faço o download executando o script ele da erro ao executar o .exe.

      Se puderes me ajudar, ficaria grato!

  • http://www.luizdeitos.com Luiz F. G. Deitos

    Opa, blz?

    Show de bola… Agora está mais legal ;o)…

    Fiz à fim de testes (obviamente), entrei em um site de uma empresa grande de Bento Gonçalves, e encontrei um código parecido, bem simplificado. Apliquei o Hack e estava eu com um arquivo php no meu desktop contendo o usuário e senha do DB…

    ruim isso hem,,, vou alertar o proprietário da empresa de tal ocorrência…

    abração!

  • Guilherme

    Cara, tava varrendo a net e não tinha sucesso. Agora consegui com o seu script.

    Vlw!

  • filipe mattevi vailo

    como eu faço para pegar o downloads direto do mysql ?

  • filipe

    olá o arquivo corronpe quando eu fasso o dowloads!

    voce sabe porque ?

  • http://wellrocha.com.br Wellington

    Me basiei no seu post para fazer o meu, no meu caso, passo um arquivo fixo para fazer download e disparo um e-mail =P

  • sergio_a

    Estou com um pequeno problema! Ao final do envio a pagina aparece meio q em codigo criptografada e o arquivo não aparece p ser salvo……….sera q alguem sabe o motivo…….

    Obrigado!

  • http://www.dourado.net Diogo

    O script esta com um erro. Ele verifica se o arquivo existe, mas como esta em outro diretorio ele nunca vai achar o arquivo e sempre vai dar a mensagem de erro.

    Deve-se concatenar o diretorio e o nome do arquivo antes da verificação.

  • http://blog.marta.preuss.nom.br Marta @suco_de_uva

    Hahahahah foi divertido achar o PHP It procurando o que eu precisava no Google!

    Valeu, Rafa!

    • https://www.facebook.com/anitachiele Anita Silveira Chiele

      A suco_de_uva programando em PHP – você deveria fazer mais isso ;p

  • Rodrigo

    Demorou mas encontrei!

    rsrs…

    Excelente artigo, muito bem explicado.

    abraço!

  • André

    Bom dia, parabéns pelo seu código, vou adicionar ele no meu site.

    Agora me diz uma coisa, como faço para adicionar uma função nesse script para baixar apenas arquivos do tipo .doc, .docx, pdf ??

  • Marcio Vinicius

    Show de bola o seu script, bastante funcional, apenas gostaria de resaltar aquilo que o Diogo disse ai em cima: "Ele verifica se o arquivo existe, mas como esta em outro diretorio ele nunca vai achar o arquivo e sempre vai dar a mensagem de erro."

    Então fiz umas pequenas modificações. Primeiro verifica se existe alguma ./ ou ../

    Após isso fiz "concatenei" o diretório com o arquivo e em seguida criei outro IF verificando se ele existe, ai depois disso eu faça o cabeçalho. Apenas Adaptei as minhas necessidades, e obrigado por compartilhar seus conhecimentos!! Seu Site é muito bom e extremamente útil!!

    Resultado final:
    http://pastebin.com/VHL0GUrB

  • Hudson

    Muito bom, corrigiu os erros anteriores..

    Já tentei vários scripts para download e sempre enfrento o mesmo problema.

    Em localhost todos funcionaram, mas quando hospedado no servidor (locaweb) o arquivo vem corrompido; no caso um arquivo XML e independente de seu tamanho vem sempre faltando seus 3 últimos caracteres.

    Alguém já usou esse script no locaweb ?

  • http://j' h'h'"

    oioksk'

  • Alciney

    pô muito bom me ajudou bastante, abraços.

  • Caíque

    Só não entendi a finalidade?

    header('Content-type: octet/stream'); e o significado.. :'[

  • Oswaldo

    O script parece ser bem interessante, mas ainda vou testá-lo pra saber se vai resolver minha necessidade. Minha intenção é conseguir realizar download de arquivos de um sistema meu e isso inclui arquivos *.exe. E preciso inclusive que estes arquivos entram diretamente num determinado diretório do computador do internauta e faça substituição de arquivos se for o caso, senão não vai funcionar. Parece vírus mas não é! A ideia é dar para os meus clientes uma atualização do software que forneci atraves do meu site na internet. Ficarei muito grato se conseguir ajuda!!!

  • http://www.labon.comb.r Tiago Nicastro

    O script não funciona para imagens que estão em CMYK. Existe algum modo de forçar o download de imagens em CMYK?

  • Leo Mendes

    Achei varios scripts na net pra forçar download de mp3 mas só este funcionou a contento!
    Muito bacana!

  • http://heliohenrique.com.br gabriel

    Muito Bom! Resolveu aqui!.

  • Cristiano

    Muito Show! Apenas um detalhe, tem que concatenar o path com o nome do arquivo antes de fazer a verificação se o arquivo existe…
    Parabéns pelo código.

  • claudiney

    o script funcionou só que o arquivo fica corrompido, como resolvo isto?

  • Luiz Henrique

    Muito bom cara, funcionou direitinho, obrigado

  • Laércio

    Ótimo artigo, ajudou-me muito.

  • MARCIO

    Cara Boa Tarde.
    Copei o seu código aCima salvei Como download.php subi o código só alterei "o diretório downloads pra o musiCa Como abaixo mas não funCiona:
    <?php
    define('DIR_DOWNLOAD', '../musica/'); // Aqui vale qualquer coisa :)

    $arquivo = $_GET['arquivo'];
    if (stripos($arquivo, './') !== false || stripos($arquivo, '../') !== false || !file_exists($arquivo))
    exit('Operação não permitida.');

    $arquivo = DIR_DOWNLOAD.$arquivo; // Aqui a gente só junta o <span class="si1e2pdjxum" id="si1e2pdjxum_16">diretório com</span> o nome do arquivo

    header('Content-type: octet/stream');
    header('Content-disposition: attachment; filename="'.basename($arquivo).'";');
    header('Content-Length: '.filesize($arquivo));
    readfile($arquivo);
    exit;
    ?>

    Não realiza o Download apraCe direto "Operação não permitida."
    ALGUEM PODE ME AJUDAR POR FAVOR…. DESDE JÁ AGRADEÇO

  • Kelwin Eggert

    Bom dia galera, só uma observação, na verificação se o arquivo existe ou não tem que inserir o diretório do arquivo para que a verificação encontre o arquivo, caso o contrário não vai deixar passar para o próximo passo.

    A linha de verificação ficou assim:

    if (stripos($arquivo, './') !== false || stripos($arquivo, '../') !== false || !file_exists(DIR_DOWNLOAD.$arquivo))
    exit('Operação não permitida.');

    Espero ter ajudado, abraços!