[TabNews – Postmortem] XSS no feed RSS e CSRF na API
Na terça-feira (29/09), fomos informados pelo Renan sobre duas vulnerabilidades no TabNews: um XSS armazenado no feed RSS e um problema de CSRF em requisições autenticadas por cookie. Os dois reports chegaram pelo GitHub (Security advisories) e também por e-mail, e as correções foram feitas na quarta-feira (30/09). 🙌
O Renan também nos ajudou no postmortem anterior sobre Open Redirect, e seguimos muito gratos por essa colaboração. 💪
Embora as falhas pudessem ser exploradas em determinados cenários, não encontramos nenhum indício de que isso tenha acontecido. Mais detalhes na seção sobre a verificação.
O que aconteceu
1. XSS armazenado no feed RSS
O XSS (Cross-Site Scripting) acontece quando um conteúdo escrito por alguém é interpretado como código pelo navegador de outra pessoa.
O feed RSS do TabNews coloca o título, a descrição e o conteúdo das publicações dentro de blocos CDATA, que servem para o XML tratar o texto como texto. Só que uma publicação com uma sequência específica de caracteres no título conseguia fechar esse bloco antes da hora, e o que vinha depois passava a ser interpretado como parte do próprio feed.
Como o feed é servido pelo próprio domínio tabnews.com.br, qualquer JavaScript poderia ser executado quando alguém abrisse o feed no navegador, como um alert ou o disparo de requisições HTTP usando a sessão da pessoa logada. Na prática, isso permitiria agir em nome dela no TabNews, realizando ações pela API.
2. CSRF em requisições autenticadas por cookie
O CSRF (Cross-Site Request Forgery) acontece quando um site de terceiros faz o navegador enviar uma requisição para outro site em que a pessoa está logada, e o navegador inclui o cookie de sessão automaticamente.
No TabNews, uma pessoa logada que acessasse uma página maliciosa em outro site poderia ter requisições feitas em seu nome, como publicar um conteúdo. Em contas com permissões elevadas, o risco também incluía ações administrativas.
Como corrigimos
Para cada problema, adicionamos mais de uma camada de proteção. Assim, se uma falhar ou for contornada, as outras continuam protegendo.
RSS
- Escape de todas as ocorrências da sequência que fecha o CDATA, no título, na descrição e no conteúdo. Essa é a correção principal e foi sugerida no report.
- Content-Security-Policy restritivo na resposta do RSS, que também foi sugerido no report. Ele impede que o navegador execute scripts ou carregue recursos a partir do feed, mesmo que algum dia surja uma nova forma de injetar conteúdo nele.
Ambas as camadas têm testes: o escape é coberto por testes unitários do gerador do feed e por um teste de integração que publica um conteúdo malicioso e verifica o XML retornado; o header de CSP tem um teste de integração próprio.
Correção feita no PR #2085, com mais casos de teste criados no PR #2087.
CSRF
- Recusa de requisições vindas de outra origem (403). Quando uma requisição com cookie de sessão altera dados (qualquer método diferente de GET, HEAD e OPTIONS), verificamos os headers Sec-Fetch-Site e Origin que o navegador envia. Se a origem for diferente da nossa, a requisição é recusada. Aplicamos essa regra a todos os métodos que alteram dados, e não só aos que foram citados no report.
- SameSite=Lax no cookie de sessão. Esse atributo pede ao navegador para não enviar o cookie em requisições originadas por outros sites (com exceção de navegações simples, como clicar em um link).
- Content-Type: application/json obrigatório (415) no login e nas requisições com cookie que alteram dados e têm corpo. Um formulário HTML não consegue enviar JSON, e uma requisição feita via JavaScript por outro site com esse header passa pelas regras de CORS do navegador.
Os três pontos têm testes de integração. Vale a ressalva de que esses testes simulam os headers da requisição e conferem o atributo SameSite do cookie, mas não reproduzem o comportamento de um navegador real. Como a maioria dos testes do TabNews, eles exercitam a API diretamente.
Correção feita no PR #2086.
Cuidado extra com a API
Queremos que a API continue acessível para casos de uso legítimos, como aplicativos e integrações, então não podemos restringir demais. Por isso:
- Requisições sem cookie de sessão não são afetadas, com exceção do login, que agora exige JSON.
- Requisições que não vêm de navegadores (e portanto não enviam Origin nem Sec-Fetch-Site) continuam funcionando normalmente.
Impacto para quem usa a API: requisições com cookie vindas de outra origem agora recebem 403, e o login e as requisições com cookie que alteram dados recebem 415 se o corpo não for JSON.
Outros problemas que encontramos
Durante as correções, encontramos e resolvemos mais alguns pontos que não estavam nos reports:
- Caracteres inválidos em XML no feed RSS. Alguns caracteres de controle não são permitidos em XML 1.0, e mesmo dentro de um CDATA eles tornam o feed ilegível para os leitores de RSS. Agora esses caracteres são removidos antes de gerar o feed.
- Limpeza da configuração de CORS. Removemos o header Access-Control-Allow-Credentials, que não tinha efeito junto com o Access-Control-Allow-Origin: *, e deixamos de permitir o header X-CSRF-Token, que nunca foi usado e podia passar a impressão de que já existia uma proteção contra CSRF.
Verificação de abuso
Para o RSS, consultamos o banco de dados em busca de publicações que explorassem a falha. Não encontramos nenhuma exploração, apenas uma tentativa antiga, sem impacto, de alguém testando se o site renderizava tags HTML (como script), e que já havia sido removida.
Para o CSRF, não há como ter 100% de certeza, mas nada fora do normal foi identificado na moderação.
O que aprendemos
- Dependências, inclusive indiretas, também podem causar problemas. Parte do comportamento que levou à falha no feed RSS vinha de bibliotecas que usamos indiretamente, e isso reforça a importância de validar o resultado final, e não só a nossa parte do código.
- Uma única camada de proteção é pouco. Para cada falha, adicionamos pelo menos duas camadas independentes, reduzindo o risco de um problema futuro.
- Não definir o SameSite do cookie deixa o comportamento a cargo do navegador. Sem o atributo, o Firefox e o Safari enviavam o cookie em requisições de outros sites, enquanto o Chrome e o Edge aplicavam Lax por padrão, mas com uma exceção de cerca de 2 minutos após a criação do cookie. Definir SameSite=Lax explicitamente deixa o comportamento igual em todos eles.
- O SameSite=Lax ajuda, mas não deve ser a única proteção. Ele ainda envia o cookie em navegações com GET e não distingue subdomínios do mesmo site, então a verificação de origem no servidor continua sendo importante.
- A proteção contra CSRF precisa equilibrar segurança e uso legítimo da API. Restringir demais poderia quebrar aplicativos e integrações.
O que ainda podemos melhorar
- Ainda não temos um Content-Security-Policy para o site inteiro. Hoje ele só existe na resposta do RSS.
Como reportar vulnerabilidades
Se você encontrar qualquer problema de segurança no TabNews, pedimos que utilize uma das opções abaixo para fazer o reporte:
- Via GitHub: Security advisories
- Por e-mail: contato@tabnews.com.br
Todos os relatos são analisados com prioridade, e agradecemos desde já qualquer colaboração nesse sentido.
Agradecimentos
Nosso agradecimento ao Renan, mais uma vez, que identificou e compartilhou as duas vulnerabilidades de forma ética, com detalhes que facilitaram a reprodução das falhas e a implementação das correções. 🤝