Secure delivery of digital products in PHP

Asked

Viewed 111 times

0

Speaking personally, I am developing a system where the customer can make the purchase and receives a link to download the product. The system has a registration area in which it is possible to upload the ZIP to a folder inside the server. However, I would like to find out how I make the downloads safe, IE, other people can not download by the same file. I thought of creating a script that duplicates this original file to a published folder and renames this file to something random, type j45287yudfre4587.zip whenever there is a purchase and releases a link for the buyer to download, after a while he automatically erases this copy. I don’t know if it’s the right way or if there’s a more professional way to do it.

  • Anderson recommend you to do the tour to understand how the community works, first try to do something, then ask the question by exposing your question and or problem.

  • Okay, let’s consider as settled, I’ll start the development and post again. Thank you.

2 answers

1

Defining the database

Since you are using a database, you can do something easier to maintain. Imagine a table called files:

+------------------------------------------+
|nome_arquivo varchar(100) not null unique||
|data_fim date                             |
|usuario_id  varchar (200)                 |
+------------------------------------------+ 

And a table users (a full authentication system will not be required, but can be expanded):

+----------------------------------------------+
|id integer not null primary key auto_increment|
|nome varchar(100) not null unique             |
|token  varchar (200) not null unique          |
+----------------------------------------------+ 

Mini file access control system

To avoid creating a login system (in the question it is not clear if you want it (but can be expanded to meet this need)) every time the user sends a new file, it will first generate a token. So we can imagine a simple form to do this, let’s call it usuario_token.php the file containing this form:

<form action="gerar_token.php" method="post">
    <div>
        <label>Nome de usuário</label>
        <input type="text" name="nome">
    </div>
    <div>
        <input type="button" value="Obter token">
    </div>
</form>

In the archive gerar_token.php would have to insert a new record in the table users, where the user name and a token for later access fields would be inserted. Thus:

<?php
$usuario_nome = null;
$usuario_token = null;

if(isset($_POST['nome'])){
    $nome = $_POST['nome'];

    //você pode ler mais sobre uniqid na referencia do php
    // em http://php.net/manual/en/function.uniqid.php
    //basicamente ela gerar um identificador unico
    //composto por caracteres alphanumericos
    $token = md5(uniqid(rand(), true));
    $id = criarUsuario($nome, $token);

    if($id !== 0){
        $conexao = new mysqli('127.0.0.1', 'root', '', 'nome_banco');
        //como não tem dados vindo do formulario que precisam ser
        //colocados na consulta, não é necessario usuar prepare statement
        $resultado = $conexao->query('select nome, token from usuarios 
                        where id = ' . $id);
        $usuario = $resultado->fetch_assoc();
        $usuario_nome = $usuario['nome'];
        $usuario_token = $usuario['toekn'];
    }else{
        echo 'Aconteceu algum erro ao salvar!';
        echo ' Pode ser qualquer coisa, nome de usuario duplicado, etc.';
    }
}

/**
@param string $nome nome do usuario vindo do formulario
@param string $token campo aleatorio gerado com a função uniqid()
@return mixed 0 caso o usuario usuario não possa ser salvo
                (nome de usuario duplicado, por exemplo), e se o
                for salvo com sucesso retornara o id do novo registro
                criado (auto_increment), onde id > 0 (maior que zero)
*/
function criarUsuario($nome, $token){
      //aqui você deve substituir pelos dados de acesso do seu banco
      $conexao = new mysqli('127.0.0.1', 'root', '', 'nome_banco');

      //para evitar injeção de sql vamos usar prepare statement
      $statement= $conexao->prepare('insert into usuarios (nome, token) 
      values (?, ?)');

      //cada interrogação no instrução acima será representada pelo 
      //tipo (string, integer, etc) e qual valor irá receber, 
      //nesse exemplo $nome e $token
      $statement->bind_param('ss', $nome, $token);

      //e por fim executamos a instrução no banco
      $status = $statement->execute();

      //se a instrução tiver executado com sucesso, retornamos o id 
      //do novo registro criado, caso contrario retornamos 0 
      //(erro ao salvar)
      if($status === true){
          return $conexao->insert_id;
      }

      return 0;
}

/******************************************************************
Agora geramos o formulario para enviar arquivos. O formulario poderia
estar em outro arquivo, mas por simplificação vai ficar aqui mesmo.

Tambem mostramos para o usuario: seu nome de usuario e seu token
para que ele possa anota-los, para futuramente usar apenas o token para
enviar arquivos (já que é mais "dificil" de adivinhar)
******************************************************************/

//só mostra o formulario se as variaveis $usuario_nome e $usuario_token
forem diferentes de null, ou seja, foram cadastrados com sucesso
if($usuario_nome === null && $usuario_token === null){
    //encerra a execução do script (não mostrando o formulario abaixo)
    exit;
}

//caso sejam diferentes de null mostra o formulario
//veja que com o if assima foi possivel "omitir" o else
?>

<div>
    <label>
        Seu nome de usuario é:<?php echo $usuario_nome; ?>
    </label>
    <label>
        Seu token de acesso é:<?php echo $usuario_token; ?>
    </label>
</div>

<form action="salvar_arquivo.php" method="post">
    <div>
        <label>Insira seu token de acesso</label>
        <input type="text" name="token">
    </div>
    <div>
        <label>Escolha um arquivo</label>
        <input type="file" name="arquivo">
    </div>

    <div>
        <label>Salvar</label>
        <input type="submit" value="Salvar">
    </div>
</form>

Most of the functions used in the archive gerar_token.php are already commented, but worth quoting again uniqid, prepare statement, last Insert id, fetch Assoc (not necessarily in order).

But back to the code structure above (gerar_token.php). In this file was created a if to check whether the registration form user had been submitted. Then a call was made to the function cadastrarUsuario() responsible for entering a new record in the table users and return the id of the inserted record. The return of this function was used to make a select in the bank and return, among others, the token saved in the database. And finally the form to send files was displayed. Just one more detail, this form (to send files) can be placed in a separate file, when the user has already been "registered". Type this (.php file.):

<form action="salvar_arquivo.php" method="post">
        <div>
            <label>Insira seu token de acesso</label>
            <input type="text" name="token">
        </div>
        <div>
            <label>Escolha um arquivo</label>
            <input type="file" name="arquivo">
        </div>

        <div>
            <label>Salvar</label>
            <input type="submit" value="Salvar">
        </div>
    </form>

And now only missing the code to save the file sent by the form and list the files of a specific user.

The archive salvar_file.php gets like this:

<?php
if(isset($_POST['token']) && isset($_FILES['arquivo'])){
    //pode ser uma pasta fora do public do seu site
    //nesse caso esta no mesmo diretorio desse arquivo
    $diretorio_arquivos = __DIR__ . '/arquivos';
    $token = $_POST['token'];
    //cria nome de arquivo unico
    $nome_arquivo = $_FILES['name'] . uniqid(rand(), true);
    $arquivo_temporario = $_FILES['tmp_name'];

    //agora checamos se o tokem existe no banco
    if(tokenExiste($token) === true){
        $conexao = new mysqli('127.0.0.1', 'root', '', 'nome_banco');
        $usuario_id = $conexao("select id from usuarios where token = '" .
                  $token . "'");
        $usuario_id = $usuario_id->fetch_assoc()['id'];
        $data_fim = '2018-01-20';

        $statement = $conexao->prepare('insert into arquivos 
        (nome_arquivo, data_fim, usuario_id) values (?, ?, ?)');
        $statement->bind_param('ssi', $nome_arquivo, $data_fim, 
        $usuario_id);
        $statement->execute();

        //move o arquivo para o diretorio definitivo
        move_uploaded_file ( $arquivo_temporario , 
        $diretorio_arquivos . '/' . $nome_arquivo );

        //da para fazer algumas validações, mas como seria
        //necessario fazer rollback no banco, deixo a seu criterio
        //implementar
        echo 'provavelmene arquivo!';
    }
    else{
       echo 'token invalido! Já fez o cadastro?';
    }
}

function tokenExiste($token){
    $conexao = new mysqli('127.0.0.1', 'root', '', 'nome_banco');
    $statement = $conexao->prepare('select token from usuarios 
                 where token = ?');
    $statement->bind_param('s', $token);
    $statement->execute();

    $statement->store_result();

    //token existe
    if($statement->num_rows == 1){
        return true;
    }

    return false;
}

The code itself is very explanatory, but it is worth highlighting references to deepen in move uploaded file, mysqli in a Rows.

And finally to list a user’s files just make a Join between the tables users and files. So in the file list.php files:

<form method="post">
    <input type="text" name="token">
    <input type="submit">
</form>

<?php
    if(isset($_POST['token'])){
        $arquivos = listaArquivos($_POST['token']);

        //não existem arquivos ou token invalido, encerra o script
        if(count($arquivos) < 1){
             echo 'não existem arquivos ou token invalido';
             exit;
        }

        //exibir arquivos em ul
        echo '<ul>';
        foreach($arquivos as $arquivo){
            echo '<li><a href="ver.php?nome=' . $arquivo 
            . '&token=' . $_POST['token'] . '">' . $arquivo
            .'</a></li>';
        }
        echo '</ul>';
    }

    function listaArquivos($token){
        $conexao = new mysqli('127.0.0.1', 'root', '', 'nome_banco');
        $statement = $conexao->prepare('select nome_arquivo from usuarios,
        arquivos where token = ? and usuario_id = id');

        $statement->bind_param('s', $token);
        $statement->execute();

        $nome_arquivo = null;
        //armazenar o nome de todos os arquivos
        $arquivos = [];

        $statement->bind_result($nome_arquivo);

        //percorre todo o resultset da consulta acima
        while ($statement->fetch()) {
            $arquivos[] = $nome_arquivo;
        }

        return $arquivos;
    }
?>

And as always deepens into bind result, fetch, statement fetch.

Finally, when you click on a link from the above list, you will be redirected to a file called see php. you will receive as parameter the name of the file to be displayed and the access token. The file see php. gets like this:

<?php
$token = $_GET['token'];
$nome_arquivo = $_GET['nome'];

//se existir o token e o nome de arquivo associados a um mesmo usuario
//a função podeAcessar retorna true
if(podeAcessar($nome_arquivo, $token)){
    $diretorio_arquivos = __DIR__ . '/arquivos';

    //enviando o arquivo para o usuario
     $mime = mime_content_type($diretorio_arquivos . '/' . $nome_arquivo);
     $tamanho = filesize($diretorio_arquivos . '/' . $nome_arquivo)

     header("Content-Type: ". $mime);
     header("Content-Length: " . $tamanho);
     //retorna o conteudo do arquivo salvo no servidor
     //dependendo do tipo de mime ("extensão") o proprio navegador exibira.
     //caso não seja compativel com o render oferecido pelo navegador 
     //será baixado
     echo file_get_contents($diretorio_arquivos . '/' . $nome_arquivo);
}else{
 echo 'parece que você não pode acessar este arquivo!';
}

function podeAcessar($nome_arquivo, $token){
    $conexao = new mysqli('127.0.0.1', 'root', '', 'nome_banco');
    $statement = $conexao->prepare('select nome_arquivo from usuarios, 
    arquivos where token = ? and usuario_id = id and nome_arquivo = ?');

    $statement->bind_param('ss', $token, $nome_arquivo);
    $statement->execute();

    $statement->store_result();

    //pode acessar
    if($statement->num_rows == 1){
        return true;
    }

    return false;
}

And to deepen the functions used consult mime content type, filesize, file_get_contents (All in the documentation of php).

With this you should have a reasonably secure system, since the files will not be in the public directory, as you can create the file folder a directory above the public folder (server root), for example, doing echo realpath($diretorio_arquivos . '/../pastadearquivos'). This allows you to access the folder grazing that is out of the server’s root. Also, to prevent injection attacks (both of sql, such as trying to send an arbitrary path).

Note: There may be syntax errors (variables missing letters) as I did not use the interpreter to validate the above codes. But you can get a general idea.

0


You can do it like this:

<a href="download.php?id=id-do-arquivo-ou-alguma-coisa-que-o-identifique">Download</a>

Right there in that file "download php." you force the download of the informed file..

Link href in php:

<a href='download.php?id={$row['file_name']}'>

Php download.php:

<?php
  // Verifique se um ID foi passado
    if(isset($_GET['ID'])) {
        // Pegue o ID$id
        $file_name= ($_GET['ID']);
        // Certifique-se de que o ID é de fato uma ID válida
    if($file_name == NULL) {
        die('O nome é inválido!');
    }
    else {
        // Conecte-se ao banco de dados
        $dbLink = new mysqli('localhost', 'root', "", 'db_name');
        if(mysqli_connect_errno()) {
            die("Falha na conexão do MySQL: ".mysqli_connect_error());
        }
         // Obtenha as informações do arquivo
        $query = "
            SELECT `type`, `file_name`, `size`, `data`
            FROM `arquivos`
            WHERE `file_name` = {$file_name}";
        $result = $dbLink->query($query);

        if($result) {
            // Verifique se o resultado é válido
            if($result->num_rows == 1) {
            // Obter a linha
                $row = mysqli_fetch_assoc($result);

                header("Content-Type: ".$row['type']);
                header("Content-Length: ".$row['size']);
                header("Content-Disposition: attachment"); 
                // Imprimir dados
                echo $row['data'];
            }
            else {
                echo 'Erro! Não existe nenhum arquivo com essa ID.';
            }
            // Livre os recursos do mysqli
            @mysqli_free_result($result);
        }
        else {
            // Se houver um erro ao executar a consulta
            echo "Erro! Falha na consulta: <pre>{$dbLink->error}</pre>";
        }
        // Cechar a conexão do banco de dados
        @mysqli_close($dbLink);
    }
}
else {
    // Se nenhuma ID passou
    echo 'Erro! Nenhuma ID foi aprovada.';
}
?>

You can do so too:

<?php

$id = $_GET[id];

if ($id == "123456") {
 header('Location: http://www.site.com.br/Arquivo.rar');
  } else {
 echo "Arquivo não encontrado";
}

?>

He will identify the "?id=123456" on your Link and will Download File "http://www.site.com.br/Arquivo.rar" Of course, you can put any id, or even more than 1 id, hence your code would have to look like this... http://www.site.com.br/download.php?id=1

<?php

$id = $_GET[id];

if ($id == "1") {
 header('Location: http://www.site.com.br/Arquivo1.rar');
  } elseif ($id == "2") {
 header('Location: http://www.site.com.br/Arquivo2.rar');
  } elseif ($id == "3") {
 header('Location: http://www.site.com.br/Arquivo3.rar');
}

?>

Browser other questions tagged

You are not signed in. Login or sign up in order to post.