Skip to content

Fragmentos em cache: dois blocos de segurança na mesma página podem receber o mesmo token #1063

Description

@rpgmem

Encontrado ao reescrever o cliente do endpoint de fragmentos (#1053, PR do DynamicFragments). Pré-existente — a reescrita preserva o comportamento de propósito, para não alargar aquela PR.

Sintoma

Numa página que combine um formulário de certificado com outro bloco de captcha — [ffc_form id=X] + [ffc_self_scheduling], ou [ffc_form id=X] + [ffc_csv_download] — o refresh de fragmentos escreve o mesmo token nos dois blocos. Como o token é de uso único desde o #1054, quem submeter primeiro queima o do outro: o segundo formulário recebe "a resposta matemática está incorreta" para uma resposta que está certa.

É a mesma classe do #1056, um nível acima: lá o refresh do agendamento trocava a pergunta em todos os formulários e o token só no primeiro; aqui os dois recebem pergunta e token idênticos.

Causa

O endpoint só emite o mapa por formulário quando há mais de um form_id:

// includes/frontend/class-ffc-dynamic-fragments.php
if ( count( $form_ids ) > 1 ) {
    // ... $fragments['captchas'][ $fid ] = ...
}

E form_ids vem de um seletor que enxerga só formulários de certificado:

// assets/js/ffc-dynamic-fragments.js
var formWrappers = document.querySelectorAll('.ffc-form-wrapper[id^="ffc-form-"]');

O bloco de segurança do agendamento e o do download de CSV não vivem dentro de um .ffc-form-wrapper, então não entram na contagem. Com um formulário de certificado + um deles: count($form_ids) === 1, nenhum mapa por formulário, e os dois blocos caem no data.captcha padrão.

O descompasso de fundo é conceitual: o cliente conta formulários de certificado, o servidor emite um desafio por formulário contado, mas quem consome um desafio é o bloco de segurança — e esses três números não são o mesmo.

Correção proposta

Contar o que de fato consome. O cliente já sabe quantos blocos existem (securityBlocks(), introduzida na PR do DynamicFragments), então:

  1. O cliente envia a quantidade de blocos, não só os form_ids.
  2. O servidor devolve essa quantidade de desafios, chamando CaptchaProvider::resolve()->challenge_payload() uma vez por bloco.
  3. O cliente distribui um por bloco, na mesma ordem, mantendo a preferência atual pelo payload específico do formulário quando houver.

Alternativa mais simples, se a mudança de protocolo não se pagar: aplicar o desafio padrão a no máximo um bloco e deixar os demais com o token renderizado pelo servidor. Um token possivelmente vencido é menos danoso que dois blocos compartilhando um — o vencido falha sozinho e o with_fresh_challenge() já devolve um novo na rejeição, enquanto o compartilhado faz um formulário derrubar o outro.

Prefiro a primeira: a segunda deixa o refresh sem fazer aquilo para que existe em parte da página.

Alcance

Precisa das duas metades — servidor e cliente — na mesma PR, senão um emite o que o outro não sabe consumir.

Cobertura

O teste JS gives each form on the page its own challenge já cobre o caso de dois formulários de certificado. Falta o caso misto: um .ffc-form-wrapper mais um bloco fora de wrapper, provando que os dois recebem tokens distintos.

Prioridade

Baixa. Exige uma página que misture os shortcodes, o que não é a configuração comum, e degrada para uma mensagem de erro com desafio novo — não perde dados nem abre acesso.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions