Uma boa estratégia para salvar logins de seus usuários com cookies

Definir um tempo limite de sessão no PHP é uma prática recomendada para garantir a segurança e eficiência das aplicações web. O tempo limite da sessão é o período em que uma sessão é considerada ativa, e após esse período de inatividade, a sessão é encerrada e o usuário precisa fazer login novamente. Aqui estão algumas razões importantes para definir um tempo limite de sessão:

- Segurança: Um tempo limite de sessão ajuda a proteger os usuários contra possíveis ataques. Se um usuário esquecer de fazer logout ou sair de uma sessão em um computador público ou compartilhado, o tempo limite garantirá que a sessão seja encerrada automaticamente após um período de inatividade, reduzindo o risco de acesso não autorizado à conta.

- Privacidade dos usuários: Limitar o tempo de uma sessão ajuda a proteger a privacidade dos usuários. Quanto mais tempo uma sessão permanecer ativa, mais informações pessoais e confidenciais podem ser armazenadas na sessão. Ao definir um tempo limite de sessão mais curto, você reduz a exposição dessas informações sensíveis.

- Conformidade com regulamentações de privacidade: Em alguns lugares do mundo, a privacidade dos dados é regulamentada por leis, como o Lei Geral de Proteção de Dados (LGPD). Essas leis exigem que os dados pessoais sejam armazenados apenas pelo tempo necessário e sejam adequadamente protegidos. Definir um tempo limite de sessão ajuda a cumprir essas obrigações regulatórias, garantindo que os dados pessoais dos usuários não sejam armazenados indefinidamente.

Ao definir o tempo limite de uma sessão PHP, você pode usar a função
session.gc_maxlifetime
para especificar o tempo em segundos. Além disso, você pode definir o valor de
session.cookie_lifetime
para controlar o tempo de expiração do cookie da sessão no navegador do usuário. Essas configurações podem ser feitas no arquivo
php.ini
ou diretamente no código PHP, usando as funções
ini_set()
ou
session_set_cookie_params()
.

É importante considerar o equilíbrio entre a segurança e a conveniência do usuário ao definir o tempo limite de sessão. Um tempo limite muito curto pode resultar em sessões frequentemente encerradas, exigindo que os usuários façam login com frequência, o que pode ser inconveniente. Portanto, é recomendável ajustar o tempo limite com base nas necessidades e características específicas da aplicação e do público-alvo. Por padrão, esse tempo é de 1440 segundos, ou 24 minutos.

Para prevenir o encerramento de sessão, e desenvolvedor os chamados "lembre-me", uma estratégia para salvar logins é usando cookies, definindo a data de expiração do cookie para uma data futura distante, tornando-o efetivamente um cookie persistente. Desta forma, o cookie permanece no dispositivo do usuário mesmo após o fechamento do navegador e pode ser usado para autenticar o usuário em visitas subsequentes ao site. No entanto, é importante observar que os cookies persistentes podem representar um risco de segurança se não forem devidamente utilizados.

O uso de cookies para salvar logins de longo prazo pode representar riscos de segurança. Se os cookies não forem devidamente protegidos e criptografados, eles podem ficar vulneráveis a ataques como cross-site scripting (XSS) e cross-site request forgery (CSRF).  Cookies persistentes também podem ser roubados por invasores que obtêm acesso ao dispositivo do usuário, permitindo potencialmente que o invasor se faça passar pelo usuário. Outro problema que costuma ocorrer, é que se as credenciais de login de um usuário forem comprometidas, o invasor pode usar o cookie persistente antigo para acessar a conta do usuário, mesmo depois que o usuário tiver alterado sua senha.

Para aumentar a segurança, recomenda-se usar os flags 
secure
HttpOnly
ao configurar cookies, e talvez o mais importante de todos, usar somente em conexões HTTPS, evitando assim que os cookies possam ser roubados numa rede compartilhada, por exemplo.

Mas falando nas estratégias de utilizar cookies para login persistente, em um tempo não muito distante, era comum encontrar sites que armazenavam somente o id o usuário no cookie. Se o cookie existisse, lia-se o id e efetuava o login. Alguns até expunham a senha pura ou com uma criptografia reversível, e efetuava o login automaticamente caso o cookie existia. Pensar nisso hoje em dia até parece um absurdo, mas ainda existem desenvolvedores criando hashs de IDUSUARIO+SENHA para criar um padrão que, teoricamente, somente ele consegue descriptografar.

Atualmente uma boa estratégia é usar um sistema de gerenciamento de sessão do lado do servidor, que gere um token de sessão exclusivo para cada usuário e o armazene em um banco de dados. O token da sessão pode ser armazenado em um cookie no dispositivo do usuário, definido para expirar após um determinado período de tempo para garantir que o usuário seja autenticado periodicamente. A utilização dessa estratégia, sem duvidas gera alguns problemas, mas com certeza é muito melhor que as estratégias usadas anteriormente. Alguns problemas desse modelo são:

Randomização insuficiente

Embora muito programadores entendam que a aleatoriedade não é tão aleatória assim, muitos outros talvez não saibam que alguns métodos aleatórios são previsíveis ou não confiaveis. Alguns problemas na randomização utilizando o PHP (e outras linguagens) são:

- Geração de números pseudoaleatórios previsíveis: O PHP usa uma função interna chamada
rand()
para gerar números pseudoaleatórios. No entanto, essa função não é considerada adequada para fins criptográficos ou de segurança, pois os números gerados podem ser previsíveis. Isso significa que, se um invasor conseguir descobrir o padrão de geração de números, ele pode reproduzir a sequência e comprometer a segurança do sistema.

- Viés nos resultados: Algumas funções de geração de números aleatórios em PHP podem apresentar viés, o que significa que certos valores têm mais probabilidade de serem gerados do que outros. Por exemplo, a função
rand()
pode não distribuir os números uniformemente em um intervalo, resultando em resultados não aleatórios.

- Falta de entropia adequada: A geração de números aleatórios requer entropia, que é uma medida de imprevisibilidade. O PHP depende do sistema operacional que está sendo executado para fornecer entropia suficiente. No entanto, em sistemas com pouca entropia disponível, a randomização em PHP pode ser comprometida e resultar em números pseudoaleatórios menos seguros.

- Problemas de compatibilidade: As funções de randomização podem variar em diferentes versões do PHP e em diferentes plataformas. Isso pode causar problemas de compatibilidade ao executar código em diferentes ambientes, resultando em resultados inconsistentes.

Para evitar esses problemas, é recomendável usar uma biblioteca externa, como o
openssl_random_pseudo_bytes
,
/dev/urandom
caso esteja utilizando sistemas compatíveis,
RandomLib
,
random_bytes
,e varias outras, que fornecem funções mais seguras e confiáveis para a geração de números aleatórios em PHP. Essas bibliotecas utilizam algoritmos criptográficos comprovados e fornecem uma fonte de entropia adequada para gerar números aleatórios seguros. Além disso, o PHP 7 introduziu a função
random_int()
, que é mais adequada para fins criptográficos, pois utiliza um gerador de números aleatórios criptograficamente seguro disponível no sistema operacional que está sendo executado.

Timing Leak ou cronometragem de tempo

Esse problema ocorre principalmente por conta dos gerenciadores de banco de dados, mas que não é um problema de banco de dados. Suponha que você verifique o token do cookie com a seguinte query:
SELECT * FROM usuarios_acessos WHERE token = 'EddggGLDWfO6ZX1rm096276ufQa7lw';
Pode parecer muito bom aos olhos de um programador sem muita experiencia. Isso porque a maneira que os sistemas de banco de dados fazem comparações de
strings
,  vão conter tempos diferentes.

Se o primeiro
E
do token estiver errado, o tempo para a verificação é 1x. Se o ultimo
w
do token estiver errado, o tempo de comparação será de 30x. Então, o atacante pode verificando o tempo das falhas, e quando a comparação for mais rapida, ele sabe que acertou o proximo caractere. Aqui tem um excelente artigo escrito por Anthony Ferrara sobre esse tipo de problema.

Atualmente com os hardwares que temos, esse tempo pode significar algo nos microsegundos. Mas se a vulnerabilidade existe, não faz sentido escrevermos um código sabendo que existe um problema, até porque, não sabemos como esse problema irá se comportar em um eventual atack DDOS ou uma janela de backup do servidor.

Para contornar isso, Paragon Initiative Enterprises sugere a utilização de 2 tokens: um para fazer a busca no banco de dados, e o outro para comparar utilizando
hash_equals()
.

Ao usar
hash_equals()
, a comparação sempre leva o mesmo tempo, independentemente dos valores dos hashes. A função funciona com diferentes algoritmos de hash suportados pelo PHP, como MD5, SHA1, SHA256, etc. Isso permite que você utilize a função para comparar valores de hash gerados por diferentes algoritmos, sem precisar se preocupar com a lógica específica de cada algoritmo de comparação. É importante ter em mente que ela deve ser usada exclusivamente para comparar valores de hash, não para comparar diretamente senhas ou outros dados sensíveis. É recomendável usar funções de hash seguras, como
password_hash()
e
password_verify()
, para o armazenamento e verificação de senhas, por exemplo.

Passo a passo de uma boa estratégia para armazenamento de logins de longo periodo

A estratégia como um todo poderia ser resumida assim:

- o usuário efetua o login, e marca a opção "me manter conectado"
- o sistema valida se o usuário e senha é válido e cria a sessão do usuário
- o sistema cria 2 hashs, uma de validação e outra de token, salva numa tabela
usuarios_acessos
, definindo o usuario, a data de criação, a hash de verificação, a hash do token, e a data de validade dessa hash
- o sistema cria o cookie no navegador do usuário, separando as hashs por linha ou por um
.
, a depender do formato da sua hash
- na proxima vez que o usuário acessar seu site, e a sessão não existir, verifique se o cookie existe
- se o cookie existir, faça a busca em
usuarios_acessos
, pela hash de verificação que ainda esteja dentro da data de validade
- caso encontrar, compare o token do cookie e o do banco de dados, usando
hash_equals()
- caso tudo estiver ok, crie a sessão de login do usuário

É importante ter em mente que quando o usuário efetuar a mudança da senha, você invalide todos esses acessos em
usuarios_acessos
. Isso porque se a senha dele for comprometida, ou se ele for alguem prevenido que troca as senhas constantemente, de nada adiantará, se o token com a senha antiga ainda for valido.

Tambem é bom lembrar, que mesmo oferecendo todos estes recursos, você ainda não conseguirá ter um sistema 100% seguro, principalmente pelos próprios erros dos seus usuários. É sempre bom lembrar de ter um bom sistema de logs, tanto em tentativas de logins inválidas, quanto as válidas, e tambem manter um controle de tempo de tentativa/erro.