Skip to content

v6.23.0

Latest

Choose a tag to compare

@github-actions github-actions released this 07 Sep 20:05
e029d6f

Security

  • O captcha matemático era contornável por replay (#1053): o token derivava apenas da resposta, sem expiração nem uso único, então um par (resposta, token) capturado uma vez autenticava qualquer envio posterior, em qualquer formulário. Agora é assinado com chave derivada do site, expira em 10 minutos e é consumido no resgate.

Added

  • Captcha ALTCHA, proof-of-work, servido inteiramente pelo próprio site (#1053): o desafio sai de um endpoint do plugin (ffc_altcha_challenge), o trabalho acontece no navegador do visitante e a verificação é PHP puro — nenhuma requisição sai do servidor. Widget MIT vendorizado em libs/js/, sem CDN. Três modos: só matemático (padrão, inalterado no upgrade), só ALTCHA, e ALTCHA com o matemático em <noscript>.
  • Aba Configurações → Captcha (#1053): escolha entre os três modos, com o efeito de cada um dito na própria opção — o modo só-ALTCHA é recusado no salvamento sem HTTPS, porque o widget se recusa a rodar fora de contexto seguro e o formulário ficaria intransponível. Fator de trabalho e validade do desafio têm piso e teto; abaixo do piso a prova não custa nada, acima do teto um celular lento mói até o widget desistir aos 90s.
  • Guarda de idempotência do dbDelta (#1087): o job fresh-install repassa cada CREATE TABLE pelo dbDelta() contra a tabela recém-criada; o que ele quiser alterar ali é um ALTER em toda ativação futura — a classe do #997, até então medida uma vez à mão e nunca mais. A primeira medição achou 14 declarações nesse estado; as 14 foram corrigidas e o gate bloqueia em zero, sem baseline.
  • Guarda de acordo entre declarações de schema (#1087): tests/Unit/SchemaAgreementTest.php compara, sem banco, o CREATE TABLE com as colunas incrementais do mesmo arquivo e as declarações da mesma tabela em arquivos diferentes — três tabelas são declaradas por mais de uma classe. Foi assim que apareceram as instalações novas nascendo incompletas, abaixo.
  • Guarda contra cast de superglobal sem tipo (#1087): tests/Unit/RequestInputCastTest.php congela em baseline os casts inteiros aplicados direto a $_POST/$_GET/$_REQUEST — 55 hoje — como catraca que só encolhe. Um cast novo falha o CI e aponta o acessor certo; um removido também falha, para travar o ganho.
  • Régua de nível 9 sobre as classes que leem linhas do $wpdb (#1060): phpstan-rows.neon.dist mais .github/scripts/phpstan-rows-report.php rodam o nível 9 na mesma árvore do gate principal e reportam só os arquivos que leem linhas. É a cegueira do #1058: $wpdb devolve toda coluna como string, e passar mixed a um parâmetro tipado só é verificado no nível 9. Entrou não-bloqueante com 243 erros e termina o ciclo bloqueando com folga zero.
  • Core\ArrayValue (#1060): leitura de escalar sobre array sem tipo — JSON decodificado, configuração de formulário, payload de token. Substitui o idioma (string) ( $data['k'] ?? '' ), que não confere nada: dado um array ele produz a string Array, dado um objeto sem __toString é fatal.
  • SettingsReader::get_string() (#1060) e RequestInput::get_post_raw_array() / has_post() (#1075): os acessores que faltavam. O primeiro recusa valor não escalar em vez de convertê-lo; o segundo devolve o container de $_POST sem sanitizar elemento nenhum, para o caller sanitizar campo a campo com a função certa de cada um — get_post_array() achata uma linha aninhada para '' e tira a marcação de um corpo que precisa de wp_kses_post(). has_post() distingue "campo ausente" de "campo vazio".

Changed

  • Formas de linha honestas em todo o repositório (#1060, #1075): as 45 classes que leem linhas do $wpdb declaram o que a linha contém, derivado coluna a coluna do CREATE TABLE — toda coluna chega como string, e uma coluna sem NOT NULL é anulável mesmo com DEFAULT. A régua de nível 9 saiu de 243 erros para zero e o job perdeu o continue-on-error. O buraco de tipo em várias delas não era o $wpdb: era o cache de objetos, cujo wp_cache_get() devolve mixed.
  • Os três acessores tipados do SettingsReader seguem uma regra só (#1060): escalar é convertido, o resto devolve o padrão. get_int()/get_bool() convertiam cego, e são a metade silenciosa do par — (int) array('45') é 1, sem aviso nenhum. Nenhum valor gravado em ffc_settings é não-escalar, então nada muda de leitura.
  • Interno (#1053) — o captcha passa a ter um contrato de estratégia (CaptchaProviderInterface + CaptchaProvider::resolve()), com o desafio matemático atrás dele. Os 6 sites de verificação e os 4 de retry não mudam: validate_security_fields() continua sendo o ponto único e agora delega a metade captcha. As duas cópias do bloco de segurança viraram uma, em templates/.
  • Interno (#1053) — o endpoint de fragmentos de página em cache passa a servir o desafio do provider configurado, em vez de chamar o captcha matemático direto; o cliente despacha pelo campo provider do payload e ignora o que não reconhece, em vez de aplicar meio payload. É o quinto site de refresh, que a unificação do contrato não havia alcançado.

Removed

  • BREAKING — o diretório html/ e seu fallback de layout foram removidos (#1087, #865): o seletor de layouts passa a ser servido exclusivamente pelo pool de modelos. Saem o glob html/*.html, a carga por nome de arquivo e o diretório inteiro. As duas condições de saída foram confirmadas em instalação real — import_legacy_templates e rewrite_html_image_refs leem 0 pendente. Quem ainda dependa de um arquivo em html/ precisa importá-lo pelo pool antes de atualizar.
  • MigrationCustomFieldsTables (#1087): criava três tabelas que os activators já criam antes dela na mesma ativação, e não fazia mais nada — nem seu get_status() tinha consumidor. Shim de quando os activators ainda não existiam. A opção que ela gravava segue no uninstall.php, porque instalações existentes a têm.
  • Shortcodes::get_new_captcha_data() (#1053): método público sem nenhum chamador em produção — o único consumidor era o próprio teste. A geração de desafio já é responsabilidade do contrato de captcha.

Fixed

  • Uma instalação nova de ffc_submissions nascia com 7 das 25 colunas (#1087): as outras 18 só chegavam depois, pelo Activator::add_columns(), na primeira mudança de FFC_VERSION. O CREATE TABLE passa a declarar as 25 mais as 12 chaves — inclusive os dois índices compostos que só existiam em instalação já atualizada.
  • Uma instalação nova de ffc_reregistration_submissions nascia sem auth_code e magic_token (#1087): as duas colunas só chegavam pelo add_columns_if_missing(), na primeira mudança de FFC_VERSION. Mesma classe da tabela de submissões.
  • Uma instalação nova de ffc_custom_fields nascia com 12 das 17 colunas (#1087): o activator do painel do usuário não declarava field_group, field_source, field_profile_key, field_mask e is_sensitive — as cinco que todo INSERT de campo personalizado grava. Saía certo só porque uma migration de execução única rodava depois, na mesma ativação. Terceira ocorrência da mesma classe, e a primeira entre declarações em arquivos diferentes.
  • Declarações de tabela que discordavam do que o banco guarda (#1087): as 14 que o gate de idempotência mediu, todas corrigidas. ffc_reregistrations declarava audience_id, coluna que o activator derruba logo depois; auth_code era declarado como índice comum nas duas tabelas em que o código o converte para UNIQUE; 20 colunas inteiras omitiam o display width que o MariaDB grava; e dez eram declaradas json, tipo que o MariaDB implementa como LONGTEXT mais um CHECK — passam a longtext, já que nenhuma consulta do plugin usa função JSON nativa.
  • Instalações carregavam dois índices em auth_code e dois em magic_token (#1087): activator e migrations indexavam as mesmas colunas sob nomes diferentes, então quem passou pelos dois caminhos paga escrita e espaço por nada. As declarações passam a concordar, o que impede instalações novas de herdar o par, e a ativação derruba os legados — sempre condicionada à existência do índice canônico, para nunca deixar a coluna sem índice.
  • O guarda de fiação AJAX não enxergava um terceiro idioma de registro (#1087): add_action( 'wp_ajax_' . static::action(), … ) resolve na subclasse, então as quatro ações dos avisos dispensáveis ficavam fora das duas direções do teste. A fiação estava correta — mas por sorte, não por verificação. Quatro guardas ganham um contrapeso que falha quando a varredura deles colapsa, em vez de passar em silêncio.
  • Um acerto no cache jogava fora o tipo que a leitura do banco declarava (#1087): wp_cache_get() devolve mixed, então seis métodos em cinco repositórios perdiam a forma da linha no caminho do cache — o achado do #1072, que nunca chegou a eles porque a régua não os enxergava. A régua media 45 classes quando 53 leem linhas: exigia a substring literal $wpdb->, então todo repositório que liga wpdb como propriedade era invisível. Corrigidos os 30 erros que estavam de pé, o gate volta a bloquear com folga zero sobre as 53 classes certas.
  • RequestInput::get_post_int()/get_get_int() liam um array como o número 1 (#1087): absint( array('45') ) é 1, em silêncio — e 1 é um id, uma contagem e um teto plausíveis. Os irmãos de string sempre conferiram o tipo; estes dois não. 74 chamadas corrigidas de uma vez, mais 39 casts diretos roteados pelos helpers — entre eles os tetos do rate limit.
  • O save do editor de formulários lia $_POST sem tipo em cinco métodos (#1084): FormEditorSaveHandler era o maior ofensor de nível 9 do repositório (120 erros). Um valor não escalar chegava direto ao sanitizador — a mensagem do limite de dispositivo, por exemplo, era gravada como a string Array mais um aviso do PHP —, e a lista de ids de localidade passava por array_map( 'sanitize_key', … ), que recebe o que a requisição aninhar ali.
  • O salvamento do calendário gravava no post meta qualquer chave que o formulário enviasse (#1075): a configuração e a de e-mail eram o array vindo do $_POST com as chaves conhecidas sobrescritas, então toda chave não prevista sobrevivia intacta até o banco. Ambas passam a ser reconstruídas a partir da lista declarada.
  • Um campo numérico enviado como array virava 1 (#1075): absint( array( '45' ) ) é 1, porque intval() de um array não vazio é 1. Um slot_duration[]=45 forjado gravava duração de slot de um minuto. Valor não numérico agora cai no padrão declarado do campo. Uma linha de horário de trabalho que não fosse array virava lixo guardado — indexar uma string por ['day'] lê o caractere 0 com aviso — e agora é descartada.
  • UserManager::get_profile() devolvia formas diferentes conforme o ramo (#1077): vindo da tabela era a linha crua — user_id como '42', mais uma chave id que ninguém lê —, e vindo do fallback era um array montado em PHP. Qual ramo roda depende do estado da instalação, não da chamada, então o mesmo código recebia tipos diferentes em sites diferentes.
  • Numa instalação nova, a aba Geral mostrava três padrões de QR Code que o plugin não usava (#1076): cada <option> do nível de correção passava o próprio valor como fallback do get_option, então com a chave ausente as quatro eram marcadas e o navegador honrava a última — a tela dizia "H — Alta (30%)" enquanto o gerador usava o 'M' declarado, e salvar sem tocar no campo gravava H. Tamanho e margem tinham a mesma divergência.
  • O guarda de defaults (#993) não enxergava as views das abas (#1076): ele varre includes/ inteiro, mas só reconhecia SettingsReader::get*() — e as views leem pelo invólucro $tab->get_option( 'chave', 'padrão' ). Passa a reconhecer as três formas, e resolve um Class::CONSTANTE no site de leitura quando o nome é inequívoco. Foi assim que o defeito acima apareceu.
  • Dois blocos de captcha na mesma página podiam receber o mesmo token (#1063): numa página que misture [ffc_form] com [ffc_self_scheduling] ou [ffc_csv_download], o refresh de fragmentos escrevia o mesmo desafio nos dois — e o token é de uso único desde o #1053, então quem enviasse primeiro queimava o do outro. A causa era conceitual: o servidor emitia um desafio por formulário, mas quem consome um desafio é o bloco de segurança. O cliente passa a informar quantos blocos existem.
  • Endpoint de fragmentos aceitava listas de tamanho ilimitado (#1063): ele é público e sem nonce por construção, então a quantidade pedida vem do atacante. form_ids acionava uma leitura de get_post_meta() por id, sem teto. Ambas as listas passam a ter limite de 20.
  • O download público de CSV recusava o captcha que acabara de aceitar (#1061): o fluxo tem duas requisições — a tela de detalhes e o download — e ambas validavam o mesmo token; com o uso único do #1053 a primeira o queimava. Conferir e gastar viraram operações distintas (peek no contrato de captcha): a tela de detalhes confere, o download consome. Um par capturado continua valendo um download só.
  • O captcha se atrapalhava com dois formulários na mesma página (#1056): o refresh do agendamento reescrevia a pergunta em todos os formulários mas, casando por id, trocava o token só do primeiro — o segundo passava a exibir uma pergunta que seu token não respondia. Agora é escopado ao formulário e casa por name. Os ids do captcha passam a ser únicos por render, corrigindo também a associação <label for> para leitores de tela.
  • Um agendamento era criado mas reportado como falha (#1058): $appointment['id'] chega do $wpdb como string e AppointmentReceiptHandler::get_receipt_url() declara int sob strict_types — o TypeError fatalava no e-mail de confirmação, depois do commit da reserva, e o visitante recebia HTTP 500 para um agendamento que existia. Uma reserva commitada nunca mais é reportada como falha.
  • Toda linha de activity log de agendamento criado era perdida (#1058): on_appointment_created() passava o id do agendamento no 4º argumento de ActivityLog::log(), que é $user_id — com fk_ffc_activity_log_user ativa o MySQL rejeitava o insert.
  • Widget do captcha aparecia em inglês, e dois controles não faziam nada (#1053): o widget resolve o idioma quando inicializa, antes de o plugin registrar as traduções, e não voltava a olhar — agora é reaplicado pelo configure() do próprio elemento. Os seletores de layout e tema saíram: bar e floating são posicionados fora da tela por construção, e o bundle não tem uma única regra que consuma theme. O tema agora vem das variáveis --altcha-* mapeadas nos tokens do plugin.
  • O widget do captcha não carregava no download público de CSV nem no agendamento (#1053): o wp_enqueue_script estava dentro de um ramo por tipo de página que só cobria o formulário de certificado e a verificação, então nos outros dois o container renderizava vazio — sem erro no console, porque um custom element que não sobe não reclama. Agora quem renderiza o widget é quem o enfileira, no momento do render.
  • O widget ALTCHA saía com quinas retas e fonte do tema, não a do formulário (#1053): as quinas curvas eram pedidas com var(--ffc-radius), um token que só existe em folhas que não carregam nessas páginas — uma variável indefinida invalida a declaração inteira em silêncio. A tipografia era herdada do tema, o que num tema de corpo 20px exibia o captcha numa fonte maior que a pergunta ao lado. CssTokenReferenceTest passa a falhar quando um var(--ffc-*) sem fallback não resolve.
  • No modo "ALTCHA + matemático", um visitante sem JavaScript via um aviso dizendo que o formulário não funcionaria (#1053): o aviso pertence ao modo só-ALTCHA, onde é verdade, e era renderizado também no composto — em vermelho, logo acima do campo matemático que o visitante deveria responder. Junto com ele saiu o painel vazio do widget, que sem JavaScript é uma caixa azul sem conteúdo.
  • A caixa do widget ALTCHA era estreita demais para o próprio conteúdo (#1053): o widget nasce com 320px e o rodapé empurra o logotipo para a direita, então numa caixa mais larga o logotipo colava no texto. Agora ocupa a largura do painel.
  • Um batch_size inválido fazia a migração se declarar concluída sem processar nada (#1060): o array de configuração chega às estratégias depois do filtro ffcertificate_migrations_registry, então um terceiro pode pôr qualquer coisa em batch_size — e (int) 'x' é 0, o que vira LIMIT 0: nenhuma linha lida, has_more falso, migração dada como completa. Agora um valor não numérico cai no padrão e há piso de 1.
  • Cursor corrompido pulava a primeira linha da migração (#1060): o cursor é uma opção, get_option() é mixed e (int) array() é 1 — a varredura id > 1 nunca via a linha 1. Um valor não numérico agora reinicia em 0, o que no pior caso relê linhas sobre as quais a migração é idempotente.
  • Uma lista de opções numérica tornava o campo select impossível de preencher (#1060): field_options é JSON escrito pelo administrador, então {"choices":[1,2]} devolvia inteiros, enquanto o validador compara o valor postado — sempre string — com in_array( …, true ). Nenhuma seleção casava e toda submissão era recusada como inválida.
  • Filtro não-escalar em ReregistrationRepository::count() era vinculado como a string Array (#1060): o mesmo defeito já corrigido em find_user_bookings(), no outro módulo, onde produzia um WHERE que não casa com nada, em silêncio. O audience_id era pior: (int) array() é 1, então um filtro inutilizável passava a contar o público 1.
  • Um valor não escalar em UserManager::update_profile() era gravado como a string Array (#1060): o patch chega sem tipo do chamador e era convertido direto; agora o campo é ignorado, o que preserva a coluna em vez de estragá-la. Mesma correção na leitura de metadados sensíveis, que entregava Array ao decifrador.
  • O cache de QR Code nunca ligava pelo toggle (#1060): os dois gravadores da chave qr_cache_enabled discordam do tipo — o save do formulário grava int 1, e o autosave, que é o que o interruptor chama de fato, grava um booleano. A checagem era 1 === $valor, e 1 === true é falso, então ligar o cache pela interface deixava-o desligado.
  • ReprintDetector::detect() devolvia date com tipo diferente conforme o ramo (#1060): inteiro (segundos unix) quando havia reimpressão, string vazia quando não. Só um consumidor lê a chave, e só no ramo de reimpressão, então o ramo vazio passou a devolver 0 — um contrato cujo tipo depende do ramo não pode ser verificado.
  • Ids não-numéricos em reservas de público viravam 0 (#1060): audience_ids e user_ids chegam dentro de um $data do chamador, e cada entrada era convertida direto com (int) — uma string solta ou null virava 0 e gravava uma linha de junção apontando para um público que não existe. Agora entradas não-numéricas são descartadas.