Como adicionar corretamente token de falsificação de solicitação entre sites (CSRF) usando PHP

96

Estou tentando adicionar alguma segurança aos formulários do meu site. Um dos formulários usa AJAX e o outro é um formulário simples de "contato". Estou tentando adicionar um token CSRF. O problema que estou tendo é que o token só aparece no "valor" HTML algumas vezes. No resto do tempo, o valor está vazio. Aqui está o código que estou usando no formulário AJAX:

PHP:

if (!isset($_SESSION)) {
    session_start();
$_SESSION['formStarted'] = true;
}
if (!isset($_SESSION['token']))
{$token = md5(uniqid(rand(), TRUE));
$_SESSION['token'] = $token;

}

HTML

 <form>
//...
<input type="hidden" name="token" value="<?php echo $token; ?>" />
//...
</form>

Alguma sugestão?

Ken
fonte
Só por curiosidade, para que token_timeserve?
zerkms
@zerkms que não estou usando atualmente token_time. Eu ia limitar o tempo dentro do qual um token é válido, mas ainda não implementei totalmente o código. Para maior clareza, removi-o da pergunta acima.
Ken
1
@Ken: então o usuário pode obter o caso ao abrir um formulário, postá-lo e obter um token inválido? (já que foi invalidado)
zerkms
@zerkms: Obrigado, mas estou um pouco confuso. Alguma chance de você me dar um exemplo?
Ken
2
@Ken: claro. Suponhamos que o token expire às 10:00. Agora são 09:59. O usuário abre um formulário e obtém um token (que ainda é válido). Em seguida, o usuário preenche o formulário por 2 minutos e o envia. Contanto que sejam 10:01 - o token está sendo tratado como inválido, portanto, o usuário obtém um erro de formulário.
zerkms

Respostas:

286

Para o código de segurança, não gere seus tokens desta forma: $token = md5(uniqid(rand(), TRUE));

Experimente isto:

Gerando um token CSRF

PHP 7

session_start();
if (empty($_SESSION['token'])) {
    $_SESSION['token'] = bin2hex(random_bytes(32));
}
$token = $_SESSION['token'];

Sidenote: Um dos projetos de código aberto do meu empregador é uma iniciativa de backport random_bytes()e random_int()em projetos PHP 5. É licenciado pelo MIT e está disponível no Github e no Composer como paragonie / random_compat .

PHP 5.3+ (ou com ext-mcrypt)

session_start();
if (empty($_SESSION['token'])) {
    if (function_exists('mcrypt_create_iv')) {
        $_SESSION['token'] = bin2hex(mcrypt_create_iv(32, MCRYPT_DEV_URANDOM));
    } else {
        $_SESSION['token'] = bin2hex(openssl_random_pseudo_bytes(32));
    }
}
$token = $_SESSION['token'];

Verificando o token CSRF

Não use apenas ==ou mesmo ===, use hash_equals()(PHP 5.6+ apenas, mas disponível para versões anteriores com a biblioteca hash-compat ).

if (!empty($_POST['token'])) {
    if (hash_equals($_SESSION['token'], $_POST['token'])) {
         // Proceed to process the form data
    } else {
         // Log this as a warning and keep an eye on these attempts
    }
}

Indo além com tokens por formulário

Você pode restringir ainda mais os tokens para que fiquem disponíveis apenas para um determinado formulário usando hash_hmac(). HMAC é uma função de hash com chave específica que é segura de usar, mesmo com funções de hash mais fracas (por exemplo, MD5). No entanto, recomendo usar a família SHA-2 de funções hash.

Primeiro, gere um segundo token para usar como uma chave HMAC e, em seguida, use uma lógica como esta para renderizá-lo:

<input type="hidden" name="token" value="<?php
    echo hash_hmac('sha256', '/my_form.php', $_SESSION['second_token']);
?>" />

E, em seguida, usando uma operação congruente ao verificar o token:

$calc = hash_hmac('sha256', '/my_form.php', $_SESSION['second_token']);
if (hash_equals($calc, $_POST['token'])) {
    // Continue...
}

Os tokens gerados para um formulário não podem ser reutilizados em outro contexto sem conhecimento $_SESSION['second_token']. É importante que você use um token separado como uma chave HMAC do que aquele que você acabou de colocar na página.

Bônus: Abordagem Híbrida + Integração Twig

Qualquer pessoa que usa o mecanismo de modelagem Twig pode se beneficiar de uma estratégia dupla simplificada, adicionando este filtro ao ambiente Twig:

$twigEnv->addFunction(
    new \Twig_SimpleFunction(
        'form_token',
        function($lock_to = null) {
            if (empty($_SESSION['token'])) {
                $_SESSION['token'] = bin2hex(random_bytes(32));
            }
            if (empty($_SESSION['token2'])) {
                $_SESSION['token2'] = random_bytes(32);
            }
            if (empty($lock_to)) {
                return $_SESSION['token'];
            }
            return hash_hmac('sha256', $lock_to, $_SESSION['token2']);
        }
    )
);

Com esta função Twig, você pode usar os tokens de uso geral como:

<input type="hidden" name="token" value="{{ form_token() }}" />

Ou a variante bloqueada:

<input type="hidden" name="token" value="{{ form_token('/my_form.php') }}" />

O Twig está preocupado apenas com a renderização do template; você ainda deve validar os tokens corretamente. Na minha opinião, a estratégia Twig oferece maior flexibilidade e simplicidade, mantendo a possibilidade de máxima segurança.


Tokens CSRF de uso único

Se você tiver um requisito de segurança de que cada token CSRF possa ser usado exatamente uma vez, a estratégia mais simples é regenerá-lo após cada validação bem-sucedida. No entanto, isso invalidará todos os tokens anteriores, o que não combina bem com pessoas que navegam em várias guias ao mesmo tempo.

A Paragon Initiative Enterprises mantém uma biblioteca Anti-CSRF para esses casos secundários. Funciona exclusivamente com tokens de uso único por formulário. Quando tokens suficientes são armazenados nos dados da sessão (configuração padrão: 65535), os tokens não resgatados mais antigos são interrompidos primeiro.

Scott Arciszewski
fonte
bom, mas como alterar o $ token após o usuário enviar o formulário? no seu caso, um token usado para a sessão do usuário.
Akam
1
Observe atentamente como github.com/paragonie/anti-csrf é implementado. Os tokens são de uso único, mas podem armazenar vários.
Scott Arciszewski
@ScottArciszewski O que você acha de gerar um resumo da mensagem a partir do id da sessão com um segredo e, em seguida, comparar o resumo do token CSRF recebido com o hash novamente do id da sessão com meu segredo anterior? Espero que você entenda o que quero dizer.
MNR de
1
Tenho uma pergunta sobre como verificar o token CSRF. I se $ _POST ['token'] estiver vazio, não devemos prosseguir, pois esta solicitação de postagem foi enviada sem o token, certo?
Hiroki
1
Porque será ecoado no formulário HTML e você deseja que seja imprevisível para que os invasores não possam simplesmente falsificá-lo. Você está realmente implementando a autenticação de desafio-resposta aqui, não simplesmente "sim, este formulário é legítimo", porque um invasor pode simplesmente falsificar isso.
Scott Arciszewski
24

Aviso de segurança : md5(uniqid(rand(), TRUE))não é uma forma segura de gerar números aleatórios. Veja esta resposta para obter mais informações e uma solução que aproveita um gerador de números aleatórios criptograficamente seguro.

Parece que você precisa de outro com seu if.

if (!isset($_SESSION['token'])) {
    $token = md5(uniqid(rand(), TRUE));
    $_SESSION['token'] = $token;
    $_SESSION['token_time'] = time();
}
else
{
    $token = $_SESSION['token'];
}
datasage
fonte
11
Nota: Eu não confiaria md5(uniqid(rand(), TRUE));em contextos de segurança.
Scott Arciszewski,
2

A variável $tokennão está sendo recuperada da sessão quando está lá

Dani
fonte