Encontrado em smoke test do develop (testes), após o merge do #1054.
Sintoma
Na página do shortcode [ffc_csv_download]:
- O visitante responde o captcha matemático → aceito, avança para a tela de detalhes do formulário.
- Clica em "Baixar CSV" → "Erro: A resposta matemática está incorreta."
O erro impede o download. Do ponto de vista do usuário é um contrassenso: a mesma resposta foi aceita um passo antes.
Causa
O download público é um fluxo de duas requisições que valida o mesmo captcha duas vezes:
| Etapa |
Handler |
Site |
| 1 — tela de detalhes |
PublicCsvDownload::ajax_info() |
class-ffc-public-csv-download.php:442 |
| 2 — download |
PublicFormsExportSource::authorize_start() |
class-ffc-public-forms-export-source.php:114 |
Antes do #1054 isso era inofensivo: o token era replayável, então validar duas vezes saía de graça. O #1054 tornou o token de uso único (ChallengeStore::redeem()), e a etapa 1 passou a queimá-lo. A etapa 2 recebe exatamente o mesmo ffc_captcha_hash — ffc-csv-download-flow.js:41 envia startData: api.formData, a string produzida por $form.serialize() na etapa 1 — e encontra o token já gasto.
Não afeta os outros consumidores do captcha: envio de formulário, verificação de certificado e agendamento são de requisição única, validam e consomem no mesmo request. O caminho sem JS do próprio CSV (handle_request(), via admin-post.php) também é de requisição única e está correto.
Correção
Separar conferir de gastar. O captcha passa a ser consumido pela ação que ele autoriza, não pela leitura de metadados que a antecede:
ChallengeStore::is_spent() — metade só-leitura do ledger.
SecurityService::peek_simple_captcha() / peek_security_fields() — conferem sem resgatar. Um token já gasto é recusado aqui também, senão a contradição só andaria um passo adiante.
CaptchaProviderInterface::peek() — no contrato, não na implementação: toda estratégia que vale a pena é de uso único (uma solução de proof-of-work é replayável até o servidor registrá-la, exatamente como o token matemático), então o ALTCHA vai precisar disso igualmente.
ajax_info() confere; authorize_start() e handle_request() consomem.
A propriedade fechada pelo #1054 fica intacta: um par (resposta, token) capturado continua valendo um download — o mesmo que vale para um visitante legítimo.
Cobertura
Guardas no nível da unidade (o ledger e o provider) e no nível do handler — ajax_info() chama peek_security_fields() e nunca validate_security_fields(). Verifiquei que a guarda falha quando o código regride.
Encontrado em smoke test do
develop(testes), após o merge do #1054.Sintoma
Na página do shortcode
[ffc_csv_download]:O erro impede o download. Do ponto de vista do usuário é um contrassenso: a mesma resposta foi aceita um passo antes.
Causa
O download público é um fluxo de duas requisições que valida o mesmo captcha duas vezes:
PublicCsvDownload::ajax_info()class-ffc-public-csv-download.php:442PublicFormsExportSource::authorize_start()class-ffc-public-forms-export-source.php:114Antes do #1054 isso era inofensivo: o token era replayável, então validar duas vezes saía de graça. O #1054 tornou o token de uso único (
ChallengeStore::redeem()), e a etapa 1 passou a queimá-lo. A etapa 2 recebe exatamente o mesmoffc_captcha_hash—ffc-csv-download-flow.js:41enviastartData: api.formData, a string produzida por$form.serialize()na etapa 1 — e encontra o token já gasto.Não afeta os outros consumidores do captcha: envio de formulário, verificação de certificado e agendamento são de requisição única, validam e consomem no mesmo request. O caminho sem JS do próprio CSV (
handle_request(), viaadmin-post.php) também é de requisição única e está correto.Correção
Separar conferir de gastar. O captcha passa a ser consumido pela ação que ele autoriza, não pela leitura de metadados que a antecede:
ChallengeStore::is_spent()— metade só-leitura do ledger.SecurityService::peek_simple_captcha()/peek_security_fields()— conferem sem resgatar. Um token já gasto é recusado aqui também, senão a contradição só andaria um passo adiante.CaptchaProviderInterface::peek()— no contrato, não na implementação: toda estratégia que vale a pena é de uso único (uma solução de proof-of-work é replayável até o servidor registrá-la, exatamente como o token matemático), então o ALTCHA vai precisar disso igualmente.ajax_info()confere;authorize_start()ehandle_request()consomem.A propriedade fechada pelo #1054 fica intacta: um par
(resposta, token)capturado continua valendo um download — o mesmo que vale para um visitante legítimo.Cobertura
Guardas no nível da unidade (o ledger e o provider) e no nível do handler —
ajax_info()chamapeek_security_fields()e nuncavalidate_security_fields(). Verifiquei que a guarda falha quando o código regride.