Forçar o download de um arquivo – Melhorado e bug corrigido (Funciona no IE 6)

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.

Ah… E a melhor parte do novo script: funciona no IE 6.

Conceito

A vulnerabilidade se baseia no fato de que você pode 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”.

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 baixa-los.

Como deter? Ta na hora de resolver este problema! ;)

Mão na massa

O que temos de fazer é bastante simples. Vamos criar um script que verifica se existe alguma string do tipo ./ (diretório atual) e ../ (diretório acima).

Caso um destes dois esteja presente, indicamos ao usuário que isto não é permitido.

Vou explicar novamente, pois vou tirar o artigo anterior do ar.

Ao recebermos o arquivo via get (download.php?arquivo=baixar.jpg), vamos coloca-lo em $arquivo.

Em seguida verificamos se não existem retornos de diretório, conforme comentado anteriormente, 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 no IE6 onde 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.

<?php
    define('DIR_DOWNLOAD', '../downloads/'); // 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 diretório com o nome do arquivo

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

Salve este arquivo com um nome simples, por exemplo download.php.
Para executar um download, envie os dados para a variável arquivo do GET.

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!

29 Responses to “Forçar o download de um arquivo – Melhorado e bug corrigido (Funciona no IE 6)”

  1. Carlos Tristacci

    Grande Jaques!

    Show o script.

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

    Abraços!

    Responder
  2. Rafael Jaques

    Eu dei uma melhorada nesse script…

    Espero que tenha ficado bom :)

    Responder
    • 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!

      Responder
  3. 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!

    Responder
  4. Guilherme

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

    Vlw!

    Responder
  5. filipe mattevi vailo

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

    Responder
  6. filipe

    olá o arquivo corronpe quando eu fasso o dowloads!

    voce sabe porque ?

    Responder
  7. 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

    Responder
  8. 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!

    Responder
  9. 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.

    Responder
  10. Marta @suco_de_uva

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

    Valeu, Rafa!

    Responder
  11. Rodrigo

    Demorou mas encontrei!

    rsrs…

    Excelente artigo, muito bem explicado.

    abraço!

    Responder
  12. 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 ??

    Responder
  13. 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

    Responder
  14. 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 ?

    Responder
  15. Alciney

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

    Responder
  16. Caíque

    Só não entendi a finalidade?

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

    Responder
  17. 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!!!

    Responder
  18. 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?

    Responder
  19. Leo Mendes

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

    Responder
  20. gabriel

    Muito Bom! Resolveu aqui!.

    Responder
  21. 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.

    Responder
  22. claudiney

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

    Responder
  23. Luiz Henrique

    Muito bom cara, funcionou direitinho, obrigado

    Responder
  24. Laércio

    Ótimo artigo, ajudou-me muito.

    Responder
  25. 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

    Responder
  26. 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!

    Responder

Deixar uma Resposta