Cuidado com Certificados TLS Não Verificados em PHP e Python

  • Sexta, 1st Abril, 2016
  • 23:44pm

Desenvolvedores web, confiam em muitos APIs de terceiros. Por exemplo, essas APIs nos permitem aceitar pagamentos de cartões de crédito, integrar mídias sociais ao site, limpar o cache do CDN. O protocolo HTTPS é utilizado para tornar segura a conexão com o servidor da API. No entanto, se o seu app não verificar o certificado TLS, alguém com más intenções pode roubar suas senhas ou os números do cartão de crédito do seu cliente.

03302016_APIserverComp_InBlogImage_V1_PT

 

Quando implementado corretamente, o protocolo TLS fornece encriptação e autenticação. A conexão entre o seu servidor e o servidor da API é encriptada usando um cipher simétrico (tipicamente AES) para que outras pessoas não consigam ler suas informações. O servidor também confirma sua identidade (autentica-se) enviando um certificado X.509 ao cliente. O cliente deve verificar a assinatura do certificado com a lista de certificados root conhecidos, mas esse passo geralmente é negligenciado. Como resultado, um ataque man-in-the-middle torna-se possível.

Se você não verificar o certificado, o atacante pode mascarar-se como o servidor da API, interceptar a informação enviada em ambas direções, ou até mesmo retornar mensagens falsas que o servidor da API nunca lhe enviou. Esse ataque já foi discutido previamente no artigo The Most Dangerous Code in the World: Validating SSL Certificates in Non-Browser Software escrito por Martin Georgiev e outros. Os autores descobriram que muitos clientes de bibliotecas de API escritas em Java e PHP não verificam os certificados corretamente, e tornam-se vulneráveis ao ataque. Os autores testaram essas bibliotecas de clientes com um certificado auto-assinado e um certificado válido que pertencia a outro nome de domínio. Eles consideraram que as raízes do problema são: API SSL contra-intuitivos (por exemplo, CURLOPT_SSL_VERIFYHOST em cURL) e bibliotecas SSL inseguras (a função fsockopen em PHP).

Ao transmitir qualquer informação pessoal ou financeira via HTTPS, assegure-se de que o certificado TLS tenha sido verificado corretamente. Há dois anos, IOActive testou 40 apps móveis de bancos e descobriram que 40% deles eram vulneráveis a um ataque MITM. Outro grupo de pesquisadores do Leibniz University of Hanover and Philipps University of Marburg descobriram que 8% dos apps de Android populares não verificam certificados. Um ataque MITM passivo contra esses apps móveis é muito real quando se usa um hotspot de WiFi público. O ataque também é possível no caso de um servidor web acessar uma API de terceiros.

PHP 5.6 consertou alguns dos problemas de verificação de certificados. Python fez o mesmo nas versões 3.4.3 e 2.7.9. Testei as novas versões para ver o que foi e o que não foi consertado. Também testei certificados revoked and expired, e adição de certificados (não incluídos na pesquisa feita por Georgiev et al.)

Testes

Vamos usar os servidores HTTPS de teste (trabalhando na hora de escrever) que desencadeiam um erro de segurança em um navegador moderno:

O site BadSSL.com contém outros casos de testes que podem ser úteis para testar novos apps.

Os scripts do teste conectam a cada servidor, usando uma configuração TLS por padrão. Você pode executar esses scripts na sua instalação para comprovar se tem problemas.

  Revoked Expired Self-signed Bad domain RC4 DH480
PHP 5.5 cURL X       X  
PHP 5.5 streams X X X X X X
PHP 5.6 cURL X          
PHP 5.6 streams X          
Python 2.7.6 (urllib, urllib2, httplib) X X X X X X
Python 2.7.6 (Requests) X X X   X X
Python 2.7.10 (urllib, urllib2, httplib, Requests) X         X
Python 3.3.0 (urllib.request, http.client) X X X X X X
Python 3.3.0 (Requests) X         X
Python 3.4.3 (urllib.request or http.client without context) X X X X    
Python 3.4.3 (urllib.request or http.client with context, Requests) X       X  
Google Go (net/http) X          

 

Todas as implementações da linguagem de programação falham para verificar se o certificado foi revogado.

A implementação do TLS no PHP 5.5 e anteriores é quebrada quando se usa funções stream (fsockopen, fopen, stream_socket_client, stream_socket_enable_crypto, ou file_get_contents). Deve-se usar funções cURL e atualizar para o PHP 5.6 assim que possível. Note que o PHP 5.5 permite o cipher RC4 desatualizado, mesmo ao usar o cURL.

Para Python, a situação é ainda mais complicada e muito mal documentada. A melhor solução seria fazer umupgrade para as versões 3.4.3 e 2.7.9 ou mais novas e sempre usar o context parameter.

Configuração TLS Recomendada para PHP

Se você puder instalar uma versão do PHP mais nova no seu servidor, faça o upgrade para PHP 5.6 ou mais novo. Isso resolverá todos os problemas de verificação, exceto os certificados revogados.

Se você não controla o software do servidor (por exemplo, está executando-o em um host compartilhado, ou sua aplicação PHP é usada por milhares de pessoas em todo o mundo), use a biblioteca cURL com as seguintes opções:

curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, TRUE);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);

Essas opções já estão configuradas para os valores corretos, assim como o cURL 7.10, mas é necessário configurar essas opções para versões mais antigas.

Streams HTTPS em PHP 5.5 ou mais antigo são inseguras e nunca devem ser usadas. Para rejeitar certificados auto-assinados, basta configurar a context option ‘verify_peer’ para TRUE, mas o atacante também pode usar um certificado de outro domínio. Algumas autoridades de certificados oferecem certificados DV gratuitos, então eles não iriam custar nada ao atacante.

Se seu código for synchronous, é possível trocar as funções stream (fsockopen e outras) por funções cURL. Se você estiver usando stream (asynchronous) I/O non-blocking com TLS ou HTTPS, atualize para o PHP 5.6 porque ele é inseguro em suas versões antigas.

Verificação de Certificado TLS em Python

Versões antigas de Python (antes de 2.7.9 ou 3.4.3) não verificam certificados. Há uma biblioteca de terceiros, que fazem pedidos para verificar certificados TLS. No entanto, não funcionam para todas as versões de Python. Nos meus testes, pedidos não conseguiram rejeitar um certificado auto-assinado ou expirado em versões anteriores a Python 2.7.6 (que é uma versão suportada de acordo com a documentação dos Pedidos).

As versões mais novas de Python deveriam ter resolvido esses problemas de verificação de TLS, mas há algumas advertências. Se você estiver usando uma versão Python 2.7.9 ou mais avançada, o certificado será verificado por padrão:

f = urlopen(url)

No entanto, esse código permite por engano qualquer certificado abaixo de Python 3.4.3 ou acima. Com o Python 3.x, você deve usar o context parameter para verificar o certificado:

f = urlopen(url, context = ssl.create_default_context())

O mesmo aplica-se ao http.client.HTTPSConnection constructor. Esse fato nunca foi mencionado em nenhuma documentação. O change log diz que cada certificado é verificado “por padrão”; a referência a biblioteca não diz nada, também.

A implementação do Python TLS também permite chaves Diffie-Hellman fracas e cipher RC4 desatualizados em muitas versões de Python (veja a tabela acima).

Colocando Certificados

É possível fortalecer a segurança do TLS ao permitir um número limitado de certificados. Por exemplo, se um servidor de API deveria sempre retornar o mesmo certificado, pode-se colocar esse código na sua aplicação.

PHP 5.6 e mais novos fornecem o context option peer_fingerprint por esse motivo (infelizmente, eles usam um hash SHA-1 fraco). Ao usar cURL, pode-se usar o parâmetro CURLINFO_CERTINFO para extrair o certificado:

if (defined('CURLOPT_CERTINFO')) {
    curl_setopt($ch, CURLOPT_CERTINFO, TRUE);
}

curl_exec($ch);

if (defined('CURLINFO_CERTINFO')) {
    $certinfo = curl_getinfo($ch, CURLINFO_CERTINFO);
    echo $certinfo[0]['Cert'];
}

O certificado pode ser hashed e comparado (usando hash_equals) com o valor conhecido. Note que esse código requer cURL 7.19.1 ou mais novo com OpenSSL (por exemplo, não vai funcionar com oOS X). No Google Go, você consegue o certificado a partir do array Response.TLS.VerifiedChains. No Python, essa tarefa envolve o uso de um módulo ssl de nível mais baixo; urllib não tem API documentado para adicionar ou retirar o certificado.

Twitter recomenda outra abordagem para suas APIs: verifique contra o número mínimo de certificados root. O CDN do Twitter usa muitos certificados assinados pela Symantec/Verisign ou Digicert, então não é possível adicionar nenhum certificado. Nesse caso, pode-se construir um arquivo de certificados root customizados e depois verificar com apenas os certificados root. Também é possível fazer o mesmo com PHP (veja a opção CURLOPT_CAINFO), Python e Google Go.

Verificações de Revogações

PHP, Python e Google Go não verificam as revogações por padrão, nem mesmo a biblioteca cURL o faz. Se o certificado foi comprometido e revogado por seu dono, você nunca saberá.

Escrever seu próprio código de verificação de revogação não é algo realista; seria necessário um criptógrafo talentoso para fazê-lo. PHP tem a opção CURLOPT_CRLFILE, mas você teria que baixar o arquivo CRL e verificar sua assinatura. OCSP stapling não é suportado em PHP. Google Go somente retorna uma resposta stapled OCSP raw no Response.TLS.OCSPResponse. Seria necessário muito trabalho para desenvolver uma função de verificação de revogação a partir dessas ferramentas.

Conclusão

O artigo The Most Dangerous Code in the World (O Código Mais Perigoso do Mundo) foi publicado em 2012. Depois disso, PHP consertou a maioria de seus problemas com TLS; Python ainda tem uma API não intuitivo e permite o uso de ciphers desatualizados.

Se quiser evitar esses problemas no seu código, atualize para a versão mais atual do PHP e teste sua aplicação com o BadSSL.com ou teste as páginas fornecidas pelo CAs.

« Retornar

ico-whatsapp
Dúvidas por WhatsApp
ico-chat
Dúvidas por Web Chat
ico-ticket.png
Abrir ticket Suporte