"Mantenha-me conectado" - a melhor abordagem

257

Meu aplicativo Web usa sessões para armazenar informações sobre o usuário após o login e para mantê-las à medida que viajam de uma página para outra no aplicativo. Nesta aplicação específica, estou armazenando o user_id, first_namee last_nameda pessoa.

Gostaria de oferecer uma opção "Manter-me conectado" no login que colocará um cookie na máquina do usuário por duas semanas, que reiniciará a sessão com os mesmos detalhes quando eles retornarem ao aplicativo.

Qual é a melhor abordagem para fazer isso? Não quero armazená- user_idlos no cookie, pois parece que seria fácil para um usuário tentar falsificar a identidade de outro usuário.

Mateus
fonte

Respostas:

735

OK, deixe-me colocar isso sem rodeios: se você estiver colocando dados do usuário ou algo derivado dos dados do usuário em um cookie para esse fim, está fazendo algo errado.

Lá. Eu disse isso. Agora podemos avançar para a resposta real.

O que há de errado com o hash de dados do usuário, você pergunta? Bem, tudo se resume à superfície de exposição e segurança através da obscuridade.

Imagine por um segundo que você é um atacante. Você vê um cookie criptográfico definido para o lembrete-me na sua sessão. Tem 32 caracteres de largura. Gee. Isso pode ser um MD5 ...

Vamos imaginar também por um segundo que eles conhecem o algoritmo que você usou. Por exemplo:

md5(salt+username+ip+salt)

Agora, tudo o que um invasor precisa fazer é forçar com força bruta o "sal" (que não é realmente um sal, mas mais sobre isso depois), e agora ele pode gerar todos os tokens falsos que quiser com qualquer nome de usuário para seu endereço IP! Mas forçar brutalmente um sal é difícil, certo? Absolutamente. Mas as GPUs modernas são extremamente boas nisso. E a menos que você use aleatoriedade suficiente nele (faça-o grande o suficiente), ele cairá rapidamente, e com ele as chaves do seu castelo.

Em resumo, a única coisa que protege você é o sal, que na verdade não o protege tanto quanto você pensa.

Mas espere!

Tudo isso foi previsto que o invasor conhece o algoritmo! Se é secreto e confuso, então você está seguro, certo? ERRADO . Essa linha de pensamento tem um nome: Segurança através da obscuridade , que NUNCA deve ser invocada.

A melhor maneira

A melhor maneira é nunca deixar as informações de um usuário deixarem o servidor, exceto a identificação.

Quando o usuário efetuar login, gere um token aleatório grande (de 128 a 256 bits). Adicione isso a uma tabela de banco de dados que mapeie o token para o ID do usuário e envie-o para o cliente no cookie.

E se o invasor adivinhar o token aleatório de outro usuário?

Bem, vamos fazer algumas contas aqui. Estamos gerando um token aleatório de 128 bits. Isso significa que existem:

possibilities = 2^128
possibilities = 3.4 * 10^38

Agora, para mostrar quão absurdamente grande esse número é, vamos imaginar todos os servidores da Internet (digamos 50.000.000 hoje) tentando forçar esse número a uma taxa de 1.000.000.000 por segundo cada. Na realidade, seus servidores derreteriam sob tal carga, mas vamos jogar isso.

guesses_per_second = servers * guesses
guesses_per_second = 50,000,000 * 1,000,000,000
guesses_per_second = 50,000,000,000,000,000

Então, 50 quadrilhões de palpites por segundo. É rápido! Certo?

time_to_guess = possibilities / guesses_per_second
time_to_guess = 3.4e38 / 50,000,000,000,000,000
time_to_guess = 6,800,000,000,000,000,000,000

Então, 6,8 sextilhões de segundos ...

Vamos tentar reduzir isso para números mais amigáveis.

215,626,585,489,599 years

Ou melhor ainda:

47917 times the age of the universe

Sim, isso é 47917 vezes a idade do universo ...

Basicamente, não será quebrado.

Entao, para resumir:

A melhor abordagem que eu recomendo é armazenar o cookie com três partes.

function onLogin($user) {
    $token = GenerateRandomToken(); // generate a token, should be 128 - 256 bit
    storeTokenForUser($user, $token);
    $cookie = $user . ':' . $token;
    $mac = hash_hmac('sha256', $cookie, SECRET_KEY);
    $cookie .= ':' . $mac;
    setcookie('rememberme', $cookie);
}

Em seguida, para validar:

function rememberMe() {
    $cookie = isset($_COOKIE['rememberme']) ? $_COOKIE['rememberme'] : '';
    if ($cookie) {
        list ($user, $token, $mac) = explode(':', $cookie);
        if (!hash_equals(hash_hmac('sha256', $user . ':' . $token, SECRET_KEY), $mac)) {
            return false;
        }
        $usertoken = fetchTokenByUserName($user);
        if (hash_equals($usertoken, $token)) {
            logUserIn($user);
        }
    }
}

Nota: Não use o token ou a combinação de usuário e token para procurar um registro no seu banco de dados. Sempre certifique-se de buscar um registro com base no usuário e use uma função de comparação com tempo seguro para comparar o token buscado posteriormente. Mais sobre ataques de tempo .

Agora, é muito importante que SECRET_KEYseja um segredo criptográfico (gerado por algo como /dev/urandome / ou derivado de uma entrada de alta entropia). Além disso, GenerateRandomToken()precisa ser uma fonte aleatória forte ( mt_rand()não é suficientemente forte. Use uma biblioteca, como RandomLib ou random_compat , ou mcrypt_create_iv()with DEV_URANDOM) ...

O hash_equals()é para evitar ataques de tempo . Se você usa uma versão PHP abaixo do PHP 5.6, a função hash_equals()não é suportada. Nesse caso, você pode substituir hash_equals()pela função timingSafeCompare:

/**
 * A timing safe equals comparison
 *
 * To prevent leaking length information, it is important
 * that user input is always used as the second parameter.
 *
 * @param string $safe The internal (safe) value to be checked
 * @param string $user The user submitted (unsafe) value
 *
 * @return boolean True if the two strings are identical.
 */
function timingSafeCompare($safe, $user) {
    if (function_exists('hash_equals')) {
        return hash_equals($safe, $user); // PHP 5.6
    }
    // Prevent issues if string length is 0
    $safe .= chr(0);
    $user .= chr(0);

    // mbstring.func_overload can make strlen() return invalid numbers
    // when operating on raw binary strings; force an 8bit charset here:
    if (function_exists('mb_strlen')) {
        $safeLen = mb_strlen($safe, '8bit');
        $userLen = mb_strlen($user, '8bit');
    } else {
        $safeLen = strlen($safe);
        $userLen = strlen($user);
    }

    // Set the result to the difference between the lengths
    $result = $safeLen - $userLen;

    // Note that we ALWAYS iterate over the user-supplied length
    // This is to prevent leaking length information
    for ($i = 0; $i < $userLen; $i++) {
        // Using % here is a trick to prevent notices
        // It's safe, since if the lengths are different
        // $result is already non-0
        $result |= (ord($safe[$i % $safeLen]) ^ ord($user[$i]));
    }

    // They are only identical strings if $result is exactly 0...
    return $result === 0;
}
ircmaxell
fonte
7
Mas essa abordagem não significa que alguém pode pegar esse nome de usuário e cookie e fazer login como usuário em qualquer outro dispositivo?
Mais simples
8
lol :-), observe que 47917 anos é o tempo máximo para adivinhar, o token aleatório também pode ser adivinhado em 1 hora.
storm_buster
33
É estranho porque seu código contradiz sua resposta. Você diz "se você está colocando dados do usuário em um cookie [...] está fazendo algo errado", mas é exatamente isso que o seu código está fazendo! Não é melhor remover o nome de usuário do cookie, calcular o hash somente sobre o token (e talvez adicionar o endereço IP para evitar o roubo de cookies) e depois fazer fetchUsernameByToken em vez de fetchTokenByUserName em rememberMe ()?
Leven
9
Desde o PHP 5.6, hash_equals pode ser usado para evitar ataques de tempo ao fazer comparações de strings.
F21
5
@Levit impede que alguém tome um token válido e altere o ID do usuário anexado a ele.
Ircmaxell 01/12/2018
93

Aviso de segurança : Basear o cookie em um hash MD5 de dados determinísticos é uma má idéia; é melhor usar um token aleatório derivado de um CSPRNG. Veja a resposta da ircmaxell a esta pergunta para uma abordagem mais segura.

Normalmente eu faço algo assim:

  1. O usuário faz login com 'mantenha-me conectado'
  2. Criar sessão
  3. Crie um cookie chamado SOMETHING contendo: md5 (salt + nome de usuário + ip + salt) e um cookie chamado somethingElse contendo id
  4. Armazenar cookie no banco de dados
  5. O usuário faz coisas e sai ----
  6. O usuário retorna, verifica se existe algo. O cookie adicional, se existir, obtém o hash antigo do banco de dados para esse usuário, verifica o conteúdo do cookie SOMETHING corresponde ao hash do banco de dados, que também deve corresponder a um hash calculado recentemente (para o ip) assim: cookieHash == databaseHash == md5 (salt + nome de usuário + ip + salt), se o fizerem, vá para 2, se não for 1

É claro que você pode usar nomes de cookies diferentes, etc. também pode alterar um pouco o conteúdo do cookie, apenas certifique-se de que não seja fácil de criar. Por exemplo, você também pode criar um user_salt quando o usuário é criado e também colocá-lo no cookie.

Além disso, você pode usar sha1 em vez de md5 (ou praticamente qualquer algoritmo)

Pim Jager
fonte
30
Por que incluir o IP no hash? Além disso, inclua informações de carimbo de data e hora no cookie e use essas informações para estabelecer uma idade máxima para o cookie, para que você não crie um token de identidade que seja bom para a eternidade.
Scott Mitchell
4
@Abhishek Dilliwal: Este é um tópico bastante antigo, mas me deparei com ele procurando a mesma resposta que Mathew. Eu não acho que usar o session_ID funcionaria para a resposta do Pim porque você não pode verificar o hash db, o hash do cookie e o session_ID atual porque o session_ID muda a cada session_start (); apenas pensei em apontar isso.
Partack 03/03
3
Sinto muito, mas qual é o objetivo do segundo cookie somethingELSE? O que é id neste caso? É apenas um tipo simples de valor "verdadeiro / falso" para indicar se o usuário deseja usar o recurso manter-me conectado? Se sim, por que não apenas verificar se o cookie ALGO existe em primeiro lugar? Se o usuário não quiser que o login persista, o cookie ALGO não estará lá em primeiro lugar, certo? Finalmente, você está gerando o hash novamente dinamicamente e comparando-o com o cookie e o banco de dados como uma medida extra de segurança?
itsmequinn
4
O token deve ser ALEATÓRIO, não conectado ao usuário / seu IP / seu useragent / qualquer coisa de qualquer forma. É uma falha de segurança importante.
pamil
4
Por que você usa dois sais? md5 (salt + nome de usuário + ip + salt)
Aaron Kreider
77

Introdução

Seu título “Mantenha-me conectado” - a melhor abordagem dificulta para mim saber por onde começar, porque se você está procurando a melhor abordagem, deve considerar o seguinte:

  • Identificação
  • Segurança

Biscoitos

Os cookies são vulneráveis. Entre as vulnerabilidades comuns de roubo de cookies do navegador e os ataques de script entre sites, devemos aceitar que os cookies não são seguros. Para ajudar a melhorar a segurança, observe que php setcookieshá funcionalidades adicionais, como

bool setcookie (string $ name [, string $ value [, int $ expire = 0 [, string $ path [, string $ domain [, string $ domain [, bool $ secure = false [, bool $ activationponly = false]]]]]]))

  • seguro (usando conexão HTTPS)
  • httponly (Reduza o roubo de identidade por meio de um ataque XSS)

Definições

  • Token (sequência aleatória imprevisível de n comprimento, por exemplo, / dev / urandom)
  • Referência (sequência aleatória imprevisível de n comprimento, por exemplo, / dev / urandom)
  • Assinatura (Gere um valor de hash com chave usando o método HMAC)

Abordagem Simples

Uma solução simples seria:

  • O usuário está conectado com Remember Me
  • Cookie de login emitido com token e assinatura
  • Quando está retornando, a assinatura está marcada
  • Se a assinatura estiver correta .., o nome de usuário e o token são pesquisados ​​no banco de dados
  • se não for válido .. retorne à página de login
  • Se válido, faça login automaticamente

O estudo de caso acima resume todos os exemplos dados nesta página, mas as desvantagens são que

  • Não há como saber se os cookies foram roubados
  • O atacante pode ter acesso a operações confidenciais, como alteração de senha ou dados, como informações pessoais e de panificação etc.
  • O cookie comprometido ainda seria válido para a vida útil do cookie

Better Solution

Uma solução melhor seria

  • O usuário está logado e lembre-se de mim está selecionado
  • Gere token e assinatura e armazene no cookie
  • Os tokens são aleatórios e são válidos apenas para autenticação única
  • O token é substituído em cada visita ao site
  • Quando um usuário não logado visita o site, a assinatura, o token e o nome de usuário são verificados
  • Lembre-se de mim, o login deve ter acesso limitado e não permitir modificação de senha, informações pessoais etc.

Código de exemplo

// Set privateKey
// This should be saved securely 
$key = 'fc4d57ed55a78de1a7b31e711866ef5a2848442349f52cd470008f6d30d47282';
$key = pack("H*", $key); // They key is used in binary form

// Am Using Memecahe as Sample Database
$db = new Memcache();
$db->addserver("127.0.0.1");

try {
    // Start Remember Me
    $rememberMe = new RememberMe($key);
    $rememberMe->setDB($db); // set example database

    // Check if remember me is present
    if ($data = $rememberMe->auth()) {
        printf("Returning User %s\n", $data['user']);

        // Limit Acces Level
        // Disable Change of password and private information etc

    } else {
        // Sample user
        $user = "baba";

        // Do normal login
        $rememberMe->remember($user);
        printf("New Account %s\n", $user);
    }
} catch (Exception $e) {
    printf("#Error  %s\n", $e->getMessage());
}

Classe usada

class RememberMe {
    private $key = null;
    private $db;

    function __construct($privatekey) {
        $this->key = $privatekey;
    }

    public function setDB($db) {
        $this->db = $db;
    }

    public function auth() {

        // Check if remeber me cookie is present
        if (! isset($_COOKIE["auto"]) || empty($_COOKIE["auto"])) {
            return false;
        }

        // Decode cookie value
        if (! $cookie = @json_decode($_COOKIE["auto"], true)) {
            return false;
        }

        // Check all parameters
        if (! (isset($cookie['user']) || isset($cookie['token']) || isset($cookie['signature']))) {
            return false;
        }

        $var = $cookie['user'] . $cookie['token'];

        // Check Signature
        if (! $this->verify($var, $cookie['signature'])) {
            throw new Exception("Cokies has been tampared with");
        }

        // Check Database
        $info = $this->db->get($cookie['user']);
        if (! $info) {
            return false; // User must have deleted accout
        }

        // Check User Data
        if (! $info = json_decode($info, true)) {
            throw new Exception("User Data corrupted");
        }

        // Verify Token
        if ($info['token'] !== $cookie['token']) {
            throw new Exception("System Hijacked or User use another browser");
        }

        /**
         * Important
         * To make sure the cookie is always change
         * reset the Token information
         */

        $this->remember($info['user']);
        return $info;
    }

    public function remember($user) {
        $cookie = [
                "user" => $user,
                "token" => $this->getRand(64),
                "signature" => null
        ];
        $cookie['signature'] = $this->hash($cookie['user'] . $cookie['token']);
        $encoded = json_encode($cookie);

        // Add User to database
        $this->db->set($user, $encoded);

        /**
         * Set Cookies
         * In production enviroment Use
         * setcookie("auto", $encoded, time() + $expiration, "/~root/",
         * "example.com", 1, 1);
         */
        setcookie("auto", $encoded); // Sample
    }

    public function verify($data, $hash) {
        $rand = substr($hash, 0, 4);
        return $this->hash($data, $rand) === $hash;
    }

    private function hash($value, $rand = null) {
        $rand = $rand === null ? $this->getRand(4) : $rand;
        return $rand . bin2hex(hash_hmac('sha256', $value . $rand, $this->key, true));
    }

    private function getRand($length) {
        switch (true) {
            case function_exists("mcrypt_create_iv") :
                $r = mcrypt_create_iv($length, MCRYPT_DEV_URANDOM);
                break;
            case function_exists("openssl_random_pseudo_bytes") :
                $r = openssl_random_pseudo_bytes($length);
                break;
            case is_readable('/dev/urandom') : // deceze
                $r = file_get_contents('/dev/urandom', false, null, 0, $length);
                break;
            default :
                $i = 0;
                $r = "";
                while($i ++ < $length) {
                    $r .= chr(mt_rand(0, 255));
                }
                break;
        }
        return substr(bin2hex($r), 0, $length);
    }
}

Teste no Firefox e Chrome

insira a descrição da imagem aqui

Vantagem

  • Melhor segurança
  • Acesso limitado ao invasor
  • Quando o cookie é roubado, é válido apenas para acesso único
  • Na próxima vez em que o usuário original acessar o site, você poderá detectar e notificar automaticamente o usuário sobre roubo

Desvantagem

  • Não suporta conexão persistente via navegador múltiplo (celular e Web)
  • O cookie ainda pode ser roubado porque o usuário só recebe a notificação após o próximo login.

Conserto rápido

  • Introdução do sistema de aprovação para cada sistema que deve ter conexão persistente
  • Use vários cookies para a autenticação

Abordagem de cookies múltiplos

Quando um invasor está prestes a roubar cookies, o único foco é em um site ou domínio específico, por exemplo. example.com

Mas, na verdade, você pode autenticar um usuário em 2 domínios diferentes ( exemplo.com e fakeaddsite.com ) e parecer "Cookie de anúncio"

  • Usuário conectado a example.com com lembre-se de mim
  • Armazenar nome de usuário, token, referência no cookie
  • Armazene nome de usuário, token, referência no banco de dados, por exemplo. Memcache
  • Enviar ID de referência via get e iframe para fakeaddsite.com
  • fakeaddsite.com usa a referência para buscar usuário e token no banco de dados
  • fakeaddsite.com armazena a assinatura
  • Quando um usuário está retornando informações de assinatura de busca com iframe de fakeaddsite.com
  • Combine os dados e faça a validação
  • ..... você sabe o restante

Algumas pessoas podem se perguntar como você pode usar 2 cookies diferentes? Bem, é possível, imagine example.com = localhoste fakeaddsite.com = 192.168.1.120. Se você inspecionar os cookies, ficaria assim

insira a descrição da imagem aqui

Da imagem acima

  • O site atual visitado é localhost
  • Ele também contém cookies definidos a partir de 192.168.1.120

192.168.1.120

  • Aceita apenas definido HTTP_REFERER
  • Aceita apenas conexão do especificado REMOTE_ADDR
  • Sem JavaScript, sem conteúdo, mas nada mais do que assinar informações e adicioná-las ou recuperá-las do cookie

Vantagem

  • 99% do tempo em que você enganou o atacante
  • Você pode bloquear facilmente a conta na primeira tentativa do invasor
  • O ataque pode ser evitado mesmo antes do próximo login, como os outros métodos

Desvantagem

  • Solicitação múltipla para o servidor apenas para um único login

Melhoria

  • Uso de uso iframe ajax
Baba
fonte
5
Embora o @ircmaxell tenha descrito a teoria muito bem, prefiro essa abordagem, pois ela funciona excelente sem a necessidade de armazenar o ID do usuário (o que seria uma divulgação indesejada) e também inclui mais impressões digitais do que apenas o ID do usuário e o hash para identificar o usuário, como o navegador. Isso torna ainda mais difícil para um invasor usar um cookie roubado. É a melhor e mais segura abordagem que já vi até agora. +1
Marcello Mönkemeyer
6

Perguntei a um ângulo de esta questão aqui , e as respostas irão levá-lo para todos os links de cookies com base em token de temporização-out que você precisa.

Basicamente, você não armazena o userId no cookie. Você armazena um token único (cadeia enorme) que o usuário usa para pegar sua sessão de logon antiga. Para torná-lo realmente seguro, você solicita uma senha para operações pesadas (como alterar a própria senha).

Dan Rosenstark
fonte
6

Segmento antigo, mas ainda uma preocupação válida. Notei algumas boas respostas sobre segurança e evitei o uso de 'segurança através da obscuridade', mas os métodos técnicos reais dados não eram suficientes aos meus olhos. Coisas que devo dizer antes de contribuir com meu método:

  • NUNCA armazene uma senha em texto não criptografado ... NUNCA!
  • NUNCA armazene a senha hash de um usuário em mais de um local no seu banco de dados. O back-end do servidor sempre é capaz de extrair a senha com hash da tabela de usuários. Não é mais eficiente armazenar dados redundantes em vez de transações de banco de dados adicionais; o inverso é verdadeiro.
  • Seus IDs de sessão devem ser exclusivos, para que nenhum usuário possa jamais compartilhar um ID, portanto, a propósito de um ID (poderia número de identificação sua carteira de motorista sempre coincidir com outra pessoas? Não.) Isso gera uma combinação única de duas peças, com base em 2 cordas únicas. Sua tabela de sessões deve usar o ID como o PK. Para permitir que vários dispositivos sejam confiáveis ​​para logon automático, use outra tabela para dispositivos confiáveis, que contém a lista de todos os dispositivos validados (veja meu exemplo abaixo) e é mapeada usando o nome de usuário.
  • Não serve para o hash de dados conhecidos em um cookie, o cookie pode ser copiado. O que procuramos é um dispositivo de usuário compatível para fornecer informações autênticas que não podem ser obtidas sem que um invasor comprometa a máquina do usuário (novamente, veja meu exemplo). Isso significaria, no entanto, que um usuário legítimo que proíbe as informações estáticas de sua máquina (por exemplo, endereço MAC, nome do host do dispositivo, agente do usuário se restrito pelo navegador, etc.) permaneça consistente (ou falsifique-a em primeiro lugar). use esse recurso. Mas se isso for uma preocupação, considere o fato de você estar oferecendo logon automático para usuários que se identificam exclusivamente, portanto, se eles se recusarem a ser conhecidos falsificando seu MAC, falsificando seu agente do usuário, falsificando / alterando seu nome de host, ocultando-se atrás de proxies etc., eles não serão identificáveis ​​e nunca deverão ser autenticados para um serviço automático. Se você quiser isso, precisará acessar o cartão inteligente fornecido com o software do lado do cliente que estabelece a identidade do dispositivo que está sendo usado.

Tudo dito, há duas ótimas maneiras de fazer logon automático no seu sistema.

Primeiro, a maneira fácil e barata de colocar tudo em outra pessoa. Se você oferecer suporte ao site para fazer login com, por exemplo, sua conta do google +, provavelmente terá um botão do google + simplificado que fará o login do usuário se ele já estiver conectado ao google (fiz isso aqui para responder a essa pergunta, como sempre conectado ao google). Se você desejar que o usuário faça login automaticamente, se já estiver conectado com um autenticador confiável e suportado, e marque a caixa para fazer isso, seus scripts do lado do cliente executam o código por trás do botão correspondente 'entrar com' antes de carregar , certifique-se de que o servidor armazene um ID exclusivo em uma tabela de logon automático com o nome de usuário, o ID da sessão e o autenticador usado para o usuário. Como esses métodos de login usam AJAX, você está esperando uma resposta de qualquer maneira, e essa resposta é uma resposta validada ou uma rejeição. Se você receber uma resposta validada, use-a normalmente e continue carregando o usuário conectado normalmente. Caso contrário, o login falhou, mas não avise o usuário, apenas continue como não logado, eles perceberão. Isso evita que um invasor que roubou cookies (ou forjou-os na tentativa de aumentar privilégios) aprenda que o usuário se autentica no site.

Isso é barato e também pode ser considerado sujo por alguns, porque tenta validar seu potencial já conectado com lugares como Google e Facebook, sem nem mesmo informar. No entanto, ele não deve ser usado em usuários que não solicitaram o login automático no site e esse método específico é apenas para autenticação externa, como no Google+ ou no FB.

Como um autenticador externo foi usado para informar ao servidor nos bastidores se um usuário foi ou não validado, um invasor não pode obter nada além de um ID exclusivo, que é inútil por si só. Vou elaborar:

  • O usuário 'joe' visita o site pela primeira vez, o ID da sessão é colocado no cookie 'session'.
  • Usuário 'joe' Efetua login, aumenta privilégios, obtém o novo ID da sessão e renova a 'sessão' do cookie.
  • O usuário 'joe' decide fazer login automaticamente usando o google +, obtém um ID exclusivo colocado no cookie 'keepmesignedin'.
  • O usuário 'joe' faz o google mantê-los conectados, permitindo que seu site faça login automaticamente no usuário usando o google no seu back-end.
  • O atacante tenta sistematicamente IDs únicos para 'keepmesignedin' (esse é um conhecimento público entregue a todos os usuários) e não está conectado a nenhum outro lugar; tenta o ID exclusivo fornecido a 'joe'.
  • O servidor recebe um ID exclusivo para 'joe', obtém correspondência no banco de dados de uma conta do google +.
  • O servidor envia o Attacker para a página de login que executa uma solicitação AJAX ao Google para efetuar login.
  • O servidor do Google recebe uma solicitação, usa sua API para ver que o Attacker não está conectado no momento.
  • O Google envia uma resposta de que não há usuário conectado atualmente por essa conexão.
  • A página do atacante recebe resposta, o script é redirecionado automaticamente para a página de login com um valor POST codificado no URL.
  • A página de login obtém o valor POST, envia o cookie para 'keepmesignedin' para um valor vazio e válido até a data de 1-1-1970 para impedir uma tentativa automática, fazendo com que o navegador do atacante simplesmente exclua o cookie.
  • O invasor recebe uma página de login normal pela primeira vez.

Independentemente do que seja, mesmo que um invasor use um ID que não exista, a tentativa deverá falhar em todas as tentativas, exceto quando uma resposta validada for recebida.

Esse método pode e deve ser usado em conjunto com o autenticador interno para aqueles que acessam o site usando um autenticador externo.

=========

Agora, para o seu próprio sistema autenticador que pode fazer login automático de usuários, é assim que eu faço:

O DB possui algumas tabelas:

TABLE users:
UID - auto increment, PK
username - varchar(255), unique, indexed, NOT NULL
password_hash - varchar(255), NOT NULL
...

Observe que o nome de usuário pode ter 255 caracteres. Meu programa de servidor limita os nomes de usuários no meu sistema a 32 caracteres, mas os autenticadores externos podem ter nomes de usuários com o @ domain.tld maior que isso, portanto, apenas ofereço suporte ao tamanho máximo de um endereço de email para obter compatibilidade máxima.

TABLE sessions:
session_id - varchar(?), PK
session_token - varchar(?), NOT NULL
session_data - MediumText, NOT NULL

Observe que não há campo de usuário nesta tabela, porque o nome de usuário, quando conectado, está nos dados da sessão e o programa não permite dados nulos. O session_id e o session_token podem ser gerados usando hashes aleatórios md5, sha1 / 128/256 hashes, carimbos de data / hora com seqüências aleatórias adicionadas a eles e depois hash, ou o que você quiser, mas a entropia de sua saída deve permanecer tão alta quanto tolerável. atenue os ataques de força bruta, mesmo que decolem, e todos os hashes gerados pela sua classe de sessão devem ser verificados quanto a correspondências na tabela de sessões antes de tentar adicioná-los.

TABLE autologin:
UID - auto increment, PK
username - varchar(255), NOT NULL, allow duplicates
hostname - varchar(255), NOT NULL, allow duplicates
mac_address - char(23), NOT NULL, unique
token - varchar(?), NOT NULL, allow duplicates
expires - datetime code

Os endereços MAC, por natureza, devem ser ÚNICOS; portanto, faz sentido que cada entrada tenha um valor único. Os nomes de host, por outro lado, podem ser duplicados em redes separadas legitimamente. Quantas pessoas usam "PC doméstico" como um de seus nomes de computador? O nome de usuário é obtido dos dados da sessão pelo servidor, portanto, é impossível manipulá-lo. Quanto ao token, o mesmo método para gerar tokens de sessão para páginas deve ser usado para gerar tokens em cookies para o logon automático do usuário. Por fim, o código de data e hora é adicionado para quando o usuário precisaria revalidar suas credenciais. Atualize essa data e hora no logon do usuário, mantendo-o dentro de alguns dias ou force-o a expirar, independentemente do último logon, mantendo-o apenas por um mês ou mais, conforme o seu design.

Isso evita que alguém falsifique sistematicamente o MAC e o nome do host de um usuário que ele conheça automaticamente. NUNCAfaça com que o usuário mantenha um cookie com sua senha, texto não criptografado ou outro. Faça com que o token seja regenerado em cada navegação da página, da mesma maneira que faria com o token da sessão. Isso reduz enormemente a probabilidade de um invasor obter um cookie de token válido e usá-lo para efetuar login. Algumas pessoas tentarão dizer que um invasor pode roubar os cookies da vítima e realizar um ataque de repetição de sessão para fazer login. Se um invasor pudesse roubar os cookies (o que é possível), certamente teria comprometido o dispositivo inteiro, o que significa que apenas poderia usá-lo para efetuar login de qualquer maneira, o que anula o objetivo de roubar cookies completamente. Desde que seu site seja executado em HTTPS (o que deveria ser feito ao lidar com senhas, números CC ou outros sistemas de login), você concedeu toda a proteção ao usuário que puder em um navegador.

Um aspecto a ter em mente: os dados da sessão não devem expirar se você usar o logon automático. Você pode expirar a capacidade de continuar a sessão falsamente, mas a validação no sistema deve retomar os dados da sessão se forem dados persistentes que se espera que continuem entre as sessões. Se você deseja dados de sessão persistentes E não persistentes, use outra tabela para dados de sessão persistente com o nome de usuário como PK e faça com que o servidor o recupere como faria com os dados normais da sessão, basta usar outra variável.

Depois que um login for alcançado dessa maneira, o servidor ainda deverá validar a sessão. É aqui que você pode codificar as expectativas para sistemas roubados ou comprometidos; os padrões e outros resultados esperados de logons nos dados da sessão geralmente podem levar à conclusão de que um sistema foi invadido ou que cookies foram forjados para obter acesso. É aqui que a sua ISS Tech pode estabelecer regras que acionariam o bloqueio de conta ou a remoção automática de um usuário do sistema de login automático, mantendo os invasores afastados o tempo suficiente para que o usuário determine como o invasor conseguiu e como cortá-los.

Como observação final, verifique se qualquer tentativa de recuperação, alteração de senha ou falha de login além do limite resulta na desativação do login automático até que o usuário valide adequadamente e reconheça que isso ocorreu.

Peço desculpas se alguém esperava que o código fosse fornecido na minha resposta, isso não vai acontecer aqui. Vou dizer que uso PHP, jQuery e AJAX para executar meus sites, e NUNCA uso o Windows como servidor ... nunca.

user253780
fonte
4

Gere um hash, talvez com apenas um segredo, e depois armazene-o no seu banco de dados para que ele possa ser associado ao usuário. Deve funcionar muito bem.

Jani Hartikainen
fonte
Esse seria um identificador exclusivo criado quando o usuário é criado ou mudaria toda vez que o usuário gerar um novo cookie "Mantenha-me conectado"?
30509 Matthew Matthew
1
A resposta de Tim Jansson descreve uma boa abordagem para a produção do hash, embora eu me sentisse mais seguro se não incluísse a senha
Jani Hartikainen
2

Minha solução é assim. Não é 100% à prova de balas, mas acho que o ajudará na maioria dos casos.

Quando o usuário efetuou login com êxito, crie uma sequência com essas informações:

$data = (SALT + ":" + hash(User Agent) + ":" + username 
                     + ":" + LoginTimestamp + ":"+ SALT)

Criptografar $data, defina o tipo como HttpOnly e defina o cookie.

Quando o usuário voltar ao seu site, siga estas etapas:

  1. Obter dados de cookies. Remova caracteres perigosos dentro do cookie. Exploda-o com :caráter.
  2. Verifique a validade. Se o cookie tiver mais de X dias, redirecione o usuário para a página de login.
  3. Se o cookie não for antigo; Obtenha o tempo de alteração da senha mais recente do banco de dados. Se a senha for alterada após o último login do usuário, redirecione o usuário para a página de login.
  4. Se o passe não foi alterado recentemente; Obtenha o agente do navegador atual do usuário. Verifique se (currentUserAgentHash == cookieUserAgentHash). Se os agentes forem iguais, vá para a próxima etapa; caso contrário, redirecione para a página de login.
  5. Se todas as etapas foram aprovadas com sucesso, autorize o nome de usuário.

Se o usuário sair, remova este cookie. Crie um novo cookie se o usuário efetuar logon novamente.

trante
fonte
2

Não entendo o conceito de armazenamento de coisas criptografadas em um cookie quando é a versão criptografada que você precisa fazer para invadir. Se estiver faltando alguma coisa, comente.

Estou pensando em adotar essa abordagem para 'Remember Me'. Se você encontrar algum problema, por favor, comente.

  1. Crie uma tabela para armazenar os dados "Lembrar de mim" - separados da tabela do usuário para que eu possa efetuar login em vários dispositivos.

  2. No login bem-sucedido (com o recurso Remember Me marcado):

    a) Gere uma sequência aleatória exclusiva para ser usada como o UserID nesta máquina: bigUserID

    b) Gere uma sequência aleatória única: bigKey

    c) Armazene um cookie: bigUserID: bigKey

    d) Na tabela "Lembrar-me", adicione um registro com: ID do usuário, endereço IP, bigUserID, bigKey

  3. Se estiver tentando acessar algo que exija login:

    a) Verifique o cookie e pesquise bigUserID e bigKey com um endereço IP correspondente

    b) Se você o encontrar, faça o login da pessoa, mas defina um sinalizador na tabela de usuário "login suave" para que, para qualquer operação perigosa, você possa solicitar um login completo.

  4. No logout, marque todos os registros "Lembrar de mim" para esse usuário como expirados.

As únicas vulnerabilidades que eu posso ver são;

  • você pode se apossar do laptop de alguém e falsificar seu endereço IP com o cookie.
  • você pode falsificar um endereço IP diferente a cada vez e adivinhar a coisa toda - mas com duas cordas grandes para combinar, isso seria ... fazer um cálculo semelhante ao anterior ... não faço ideia ... grandes probabilidades?
Enigma Plus
fonte
Olá, e obrigado por esta resposta, eu gosto. Porém, uma pergunta: por que você precisa gerar duas seqüências aleatórias - bigUserID e bigKey? Por que você não gera apenas 1 e o usa?
Jeremy Belolo
2
bigKey expira após um período de tempo predefinido, mas bigUserID não. bigUserID é permitir que você tenha várias sessões em dispositivos diferentes no mesmo endereço IP. Esperança de que faz sentido - eu tinha que pensar por um momento :)
Enigma Além disso,
Uma coisa que o hmac pode ajudar é que, se você encontrou o hmac adulterado, certamente sabe que alguém tentou roubar cookies e, em seguida, é possível redefinir todos os estados de login. Estou certo?
precisa
2

Li todas as respostas e ainda achava difícil extrair o que deveria fazer. Se uma imagem vale 1k palavras, espero que isso ajude outras pessoas a implementar um armazenamento persistente seguro com base nas práticas recomendadas de cookie persistente aprimorado de Barry Jaspan

insira a descrição da imagem aqui

Se você tiver dúvidas, comentários ou sugestões, tentarei atualizar o diagrama para refletir o novato que está tentando implementar um login persistente seguro.

Josh Woodcock
fonte
0

A implementação do recurso "Mantenha-me conectado" significa que você precisa definir exatamente o que isso significa para o usuário. No caso mais simples, eu usaria isso para significar que a sessão tem um tempo limite muito mais longo: 2 dias (digamos) em vez de 2 horas. Para fazer isso, você precisará de seu próprio armazenamento de sessão, provavelmente em um banco de dados, para poder definir tempos de expiração personalizados para os dados da sessão. Em seguida, você precisa definir um cookie que permanecerá por alguns dias (ou mais), em vez de expirar quando fecharem o navegador.

Eu posso ouvir você perguntando "por que 2 dias? Por que não 2 semanas?". Isso ocorre porque o uso de uma sessão no PHP automaticamente atrasa a expiração. Isso ocorre porque a expiração de uma sessão no PHP é na verdade um tempo limite inativo.

Agora, tendo dito isso, provavelmente implementaria um valor de tempo limite mais difícil que armazene na própria sessão e após duas semanas ou mais e adicionaria código para ver isso e invalidar à força a sessão. Ou pelo menos para desconectá-los. Isso significa que o usuário será solicitado a fazer login periodicamente. Yahoo! faz isso.

staticsan
fonte
1
A definição de uma sessão mais longa é provavelmente ruim porque desperdiça recursos do servidor e vai afetar o desempenho negativamente
user3091530
0

Eu acho que você poderia fazer isso:

$cookieString = password_hash($username, PASSWORD_DEFAULT);

Loja $cookiestring no banco de dados e defina-o como um cookie. Defina também o nome de usuário da pessoa como um cookie. O ponto principal de um hash é que ele não pode ter engenharia reversa.

Quando um usuário aparecer, obtenha o nome de usuário de um cookie e não $cookieStringde outro. Se $cookieStringcorresponder ao armazenado no banco de dados, o usuário será autenticado. Como password_hash usa um sal diferente a cada vez, é irrelevante quanto ao texto não criptografado.

Hayden O'Sullivan
fonte