Criando mecanismos de autenticação seguros

Quem sou eu?

Agenda

  • Protegendo CSRF de forma correta;
  • CAPTCHA;
  • Duplo fator de autenticação;
  • Prevenção à enumeração de usuários;
  • Funcionalidade "Permanecer logado";
  • Funcionalidade "Esqueci a senha".
  • Login via terceiros;
  • Autenticação em APIs.

Protegendo CSRF de forma correta

(Cross-site request forgery)

O que é na prática?

  • Imagine que você esteja logado em um site site1.com
  • Então acessa o site site2.com
  • E no site site2.com, há uma requisição POST site1.com/transferir?conta=1234&valor=1000
  • Como você está autenticado no site1, essa requisição será válida
  • E você irá perder seu precioso dinheiro!
Mas por que eu preciso disso na tela de login?

Para garantir que um atacante não faça um usuário passar-se por ele

Como se proteger? (continuação)

  • Crie uma sessão "falsa"
  • Gere um token para ele e guarde-o nessa sessão
  • Coloque esse token junto da requisição (em campos do formulário ou cabeçalhos do AJAX)


Como se proteger? (continuação)

  • Na página que recebe a ação de login, verifique se o token enviado é o mesmo da sessão
  • Se quiser, verifique também o cabeçalho Referer (ou Origin) da requisição para garantir que a origem é do seu site

Premissas e recomendações (continuação)

  • O token não pode se repetir entre usuários diferentes, então é importante utilizar um CSPRNG (Cryptographically Secure Pseudo-Random Number Generator)
  • Não pode ser muito pequeno (para evitar brute force)
  • Não pode aparecer nos logs do servidor
  • Não pode estar na URL

Premissas e recomendações (continuação)

  • Você pode gerar tokens a cada requisição ao invés de um por sessão, mas isso interfere na usabilidade do sistema (por exemplo, o botão "Voltar" do navegador irá causar uma falha de CSRF)
  • Se você quiser uma abordagem stateless, pode gerar um token criptografado com o Session ID do usuário e um timestamp, por exemplo


Referências



Bibliotecas

CAPTCHA

CAPTCHA do RapidShare

Saudades, RapidShare

CAPTCHA bugs

Definição

Completely
Automated
Public
Turing test to tell
Computers and
Humans
Apart

Bibliotecas

Duplo fator de autenticação

Google Authenticator

HOTP

  • HMAC-based One-Time Password é um algoritmo baseado em HMAC que gera uma senha a partir de uma chave
  • Para garantir a unicidade (afinal, há One-Time no nome), é incrementado um contador a cada geração/checagem do valor

TOTP

  • Time-based One-Time Password é uma extensão do HOTP, que utiliza o horário atual ao invés do contador para a unicidade
  • Geralmente, há uma janela de 30 segundos de validade da senha
use Sonata\GoogleAuthenticator\GoogleAuthenticator;
use Sonata\GoogleAuthenticator\GoogleQrUrl;

/* class { ... */
public function __construct() {
    $this->authenticator = new GoogleAuthenticator();
    $this->secret = getSecretFromSomewhere();
}

public function check(string $code) : bool {
    return $this->authenticator->checkCode(
        $this->secret,
        $code
    );
}

public fuction generateSecret() : string {
    return $this->authenticator->generateSecret();
}

public fuction generateSecret(string $secret) : string {
    return GoogleQrUrl::generate('My App', $secret);
}

Bloqueie o usuário após algumas tentativas inválidas do token do 2FA!

Por que bloquear?

  • Tokens de 6 dígitos possuem 1.000.000 de combinações (000000 - 999999)
  • Se um atacante fizer 1 requisição por segundo ao seu sistema, ele irá demorar 1 milhão de segundos para testar todas as combinações possíveis
  • 1000000 / 86400 = 11.574074074
  • Ou seja, o token será quebrado em menos de 12 dias
  • Mas que atacante só faz 1 requisição por segundo?

Referências, bibliotecas e soluções

O que um sistema que rode o código a seguir pode dar de informação a um atacante?
public function login(string $username, string $password) : bool
{
    $user = $this->findByUsername($username);
    if (! $user) {
        return false;
    }
    return $this->verifyPassword(
        $password,
        $user->getPassword()
    );
}

É possível saber que usuários existem em sua aplicação através de timing attack

Modifique o código para que a custosa função verifyPassword seja sempre invocada!

Prevenção à enumeração de usuários

public function login(string $username, string $password) : bool
{
    $storedPassword = $this->generateFakePassword();
    $user = $this->findByUsername($username);
    if ($user) {
        $storedPassword = $user->getPassword();
    }
    return ($this->verifyPassword($password, $storedPassword))
        && ($user !== null);
}

Será que não estamos exagerando?
É realmente possível ver essa diferença?


Vamos testar, então!

Executando os scripts login-user-enumeration.php
e login-user-enumeration-fixed.php

Funcionalidade "Permanecer logado"

Solução mais simples

  • Gerar um token único
  • Guardá-lo no cookie do browser do usuário
  • Salvar esse token em um banco de dados relacionando ao usuário
  • Sempre que é feito o acesso à tela de login, comparar os tokens

Cuidados

  • Garanta que o token será único para o usuário e não previsível (novamente, use CSPRNG)
  • Faça comparações que tenham tempo fixo para prevenir timing attacks (por exemplo, hash_equals())
  • Precisa ser eficiente, para não permitir DoS

Solução mais segura

1. Prefixe o token com um identificador (NÃO use diretamente o ID do usuário)

$identifier = hash('sha256', $userId); // 64 bytes
$token = bin2hex(random_bytes(32)); // 64 bytes
$cookie = $identifier . $token;

2. Crie uma tabela no banco de dados com o identificador acima e somente o hash do token ao invés dele

CREATE TABLE `rememberme_tokens` (
  `id` int unsigned AUTO_INCREMENT,
  `identifier` char(64) NOT NULL,
  `token_hash` char(128) NOT NULL,
  PRIMARY KEY (`id`)
)
$hashedToken = hash('sha512', $token);
$pdo->prepare('INSERT INTO rememberme_tokens (NULL, ?, ?)')
    ->execute([$identifier, $hashedToken]);

3. Ao consultar o banco, utilize apenas o identificador

$query = $pdo->prepare(
    'SELECT `token_hash` FROM `rememberme_tokens`
        WHERE `identifier` = ? LIMIT 1'
);
$hashFromDatabase = $query->fetch(PDO::FETCH_COLUMN);

4. Faça o hash do token informado no cookie do usuário e compare-o com o que está no banco (com hash_equals())

$hashedToken = hash('sha512', substr($cookie, 64));
if (hash_equals($hashedToken, $hashFromDatabase)) {
    redirectToHome();
} else {
    redirectToLogin();
}

Referência e biblioteca

Funcionalidade "Esqueci a senha"

Precauções

  • Perguntas secretas estão cada vez menos secretas por causa das redes sociais
  • Emails não são sempre encriptados
  • Emails podem não chegar
  • SMS não são confiáveis

Prepare-se!

  • Realmente avalie se seu sistema precisa dessa funcionalidade automática
  • NÃO crie uma nova senha e envie por email
  • NÃO mande a senha em texto plano por email (o quêêê?????)
  • Se for fornecido um usuário que não existe na base, não o avise!

Tentativa de
Solução

  • Utilize a mesma abordagem do token descrita no slide de "Permanecer logado"
  • Coloque um tempo limite para expiração do token
  • Envie o código para o usuário (se for por email, criptografe!)

Referências e biblioteca

Login via terceiros

Social Login

Referências e bibliotecas

Autenticação em APIs

Dicas (continuação)

  • Use HTTPS sempre (♥ Let's Encrypt)
  • Não crie seu próprio sistema de autenticação, utilize um protocolo já conhecido (OpenID, OAuth 2.0, entre outros)
  • Para geração dos identificadores do usuário (por exemplo, client secret no OAuth 2) utilize CSPRNG

Dicas (continuação)

  • Para tokens, utilize JWT ou Paseto
  • Se for usar JWT, garanta que seu sistema não permita um algorithm=none
  • Mesmo se usar um API Gateway, não deixe seus microserviços sem autenticação. Repasse o token para todos!

Referências



Bibliotecas

Recapitulando...

  • Login seguro não é trivial
    Login seguro não é fácil
    Login seguro não é para leigos
  • Mas é possível!

Slides

QR Code
viniciuscampitelli.com