Security Blog
Posted By Gregory

Under The Hood 9.8.3


English version: Under The Hood 9.8.3


Esta versão é um ciclo de manutenção e reforço de segurança, e não um lançamento de novos recursos. A maior parte do trabalho foi realizada em áreas que os proprietários de sites raramente observam: a camada de dados que lê seus registros de segurança, o código que compõe os e-mails de notificação e os caminhos de exportação que fornecem um arquivo CSV com seus registros de atividade. Nada disso altera a aparência do plugin, mas tudo isso melhora a confiabilidade e a segurança do plugin quando um site está sobrecarregado, quando um registro é grande ou quando um invasor explora uma vulnerabilidade que ainda não havíamos corrigido.

A seguir, apresentamos um relato detalhado da implementação para administradores e desenvolvedores que desejam entender o que realmente mudou.

Fechamento de um vetor de injeção de destinatário de e-mail

O WP Cerber envia dois tipos de e-mail transacional que incorporam o nome da pessoa: a mensagem de PIN de autenticação de dois fatores e a notificação de Alerta de Atividade. Ambos constroem o destinatário no formato familiar da RFC 5322, Name <email> , e ambos obtêm esse nome diretamente dos campos de perfil do WordPress ( user_firstname , user_lastname e display_name ) sem usar caracteres de escape.

O problema reside na forma como a wp_mail() lida com um destinatário do tipo string. Ela divide a string em uma vírgula literal e não respeita os limites das aspas. Um usuário que inserisse os caracteres corretos em seu próprio nome de perfil poderia, portanto, injetar um endereço extra na lista de destinatários do e-mail com o PIN de autenticação de dois fatores (2FA) gerado por CRB_2FA::send_user_pin() e do e-mail de alerta gerado pela ramificação ` user_list da função cerber_get_email() . Em um site onde qualquer pessoa pode se cadastrar e definir um nome de exibição, essa é uma maneira real de redirecionar silenciosamente uma cópia de uma mensagem de segurança.

Adicionamos um sanitizador dedicado, crb_sanitize_mail_display_name() , em cerber-common.php e o aplicamos em ambos os pontos de chamada. Ele remove exatamente os caracteres que importam para esse tipo de injeção: a vírgula, as aspas, a barra invertida, os colchetes angulares e os caracteres de controle. O nome visível ainda é renderizado normalmente para usuários legítimos e a lista de destinatários não pode mais ser manipulada por um campo de perfil criado especificamente para esse fim.

Removendo um vetor XSS armazenado no verificador de propriedade.

O verificador de malware informa quando a propriedade de um plugin instalado é alterada no repositório WordPress.org. Para isso, ele utiliza os metadados de propriedade fornecidos por esse repositório e, até então, incorporava alguns desses metadados ao aviso administrativo como HTML bruto, incluindo tags de âncora criadas manualmente para links de perfil do proprietário.

Esse repositório é uma fonte confiável e a probabilidade de metadados maliciosos é baixa, mas dados externos nunca devem chegar à interface administrativa como marcação não escapada. Tratá-lo como confiável é justamente a suposição da qual preferimos não depender.

Para fechar o vetor corretamente em vez de corrigi-lo localmente, introduzimos um novo elemento de fábrica de interface do usuário, ` formatted_text , construído por meio da função auxiliar crb_ui_formatted_text() . Ele renderiza um modelo de texto simples que contém marcadores de posição numerados %N$s . Fragmentos literais do modelo e argumentos escalares são escapados em HTML, enquanto qualquer argumento que seja um elemento de interface do usuário é renderizado por meio do renderizador ativo. Portanto, cada valor dinâmico é escapado no contexto em que é efetivamente emitido, que é a única maneira confiável de evitar erros de contexto de saída.

A mensagem de propriedade em crb_check_ownership() agora usa este elemento, e os links de perfil do proprietário são construídos com crb_ui_link() em vez de strings âncora concatenadas, portanto, URLs de perfil e nomes de exibição são escapados como URLs e como texto, respectivamente. A string de tradução permanece inalterada, então as localizações continuam funcionando. O risco de XSS de administrador armazenado foi eliminado.

Corrigindo o filtro "Qualquer erro de software" no Inspetor de Tráfego

No Log do Inspetor de Tráfego, o formulário de Busca Avançada permite combinar diversas condições. Uma delas é a caixa de seleção "Qualquer erro de software". Quando essa caixa de seleção era combinada com outros filtros, solicitações que apresentavam um erro de PHP registrado podiam aparecer nos resultados, mesmo que não correspondessem às outras condições especificadas.

A causa raiz era a precedência de operadores na forma como a cláusula WHERE era montada. O código legado concatenava os fragmentos de filtro como strings, e a condição de erro agrupada não era colocada entre parênteses, permitindo que um OR dentro dela escapasse da lógica AND circundante. Como parte da migração da consulta de tráfego para o construtor de consultas, a condição de erro agrupada agora é colocada corretamente entre parênteses, e o filtro combinado se comporta conforme o esperado. Ao refinar uma pesquisa, os resultados agora respeitam todas as condições definidas.

Correspondência literal de curingas de pesquisa

A busca avançada no registro de tráfego permitia anteriormente que um % ou _ digitado em um termo de busca funcionasse como um metacaractere LIKE , porque o termo era inserido no padrão sem ser escapado. Isso é, na melhor das hipóteses, um recurso acidental e, na pior, um padrão de carga desnecessário.

Agora, cada termo LIKE passa por CRB_Database::escape_like() antes que os curingas ao redor sejam adicionados, de modo que % e _ sejam interpretados como os caracteres literais digitados pelo usuário. As buscas se comportam de forma previsível e a consulta não pode ser forçada a examinar mais do que o pretendido.

Correção do tratamento de limites de memória para exportações

A exportação de um registro de atividades ou tráfego grande consome muita memória, por isso o WP Cerber aumenta a memória disponível antes da operação. Em alguns ambientes, um valor numérico de limite de memória, como 512 , era interpretado como bytes em vez de megabytes. Quando isso acontecia, o plugin não conseguia aumentar o limite conforme o esperado e a exportação podia ser interrompida antes do previsto.

O valor agora é interpretado com a unidade correta, portanto o aumento de memória se aplica conforme o planejado e as exportações de grande porte são concluídas nos ambientes que eram afetados anteriormente.

Reconstruindo as exportações de logs em torno do streaming.

As exportações de Atividade e Tráfego costumavam ler as linhas correspondentes em blocos, executando novamente o mesmo SELECT com um OFFSET crescente para cada bloco. Em um log grande, isso se degrada em uma varredura de offset profundo, onde cada bloco custa mais do que o anterior e mantém um resultado crescente na memória.

Ambas as exportações agora leem suas linhas em uma única passagem não armazenada em buffer por meio de CRB_Database::query_stream() , encapsulada em um gerador que produz uma linha por vez. O uso de memória permanece constante, independentemente da quantidade de registros correspondentes, e o banco de dados realiza o trabalho apenas uma vez, em vez de uma vez por bloco. Como um fluxo não armazenado em buffer bloqueia a conexão enquanto está sendo consumido, o total de linhas e o intervalo de datas são resolvidos antecipadamente com consultas COUNT e MIN / MAX separadas e armazenadas em buffer, construídas a partir dos mesmos filtros, antes que o fluxo seja aberto.

Dois cabeçalhos de resposta foram adicionados à função auxiliar de download de arquivos compartilhada, crb_file_headers() . X-Accel-Buffering: no instrui o Nginx, quando este atua como proxy para PHP-FPM, a encaminhar cada fragmento imediatamente em vez de armazenar em buffer toda a exportação antes de enviá-la. Isso melhora o tempo até o primeiro byte (TTFB) e evita que o proxy mantenha um arquivo CSV grande na memória. O cabeçalho é específico do Nginx e é ignorado sem problemas pelo Apache com mod_php e por outros proxies. Cache-Control: no-store impede que o navegador ou qualquer proxy intermediário armazene em cache uma exportação de log de segurança sensível.

Também tornamos a limpeza do fluxo determinística. A exportação consome o gerador dentro de um bloco try / finally e libera o único identificador restante no bloco finally , de modo que o resultado não armazenado em buffer seja liberado e a conexão seja desbloqueada em todos os caminhos de saída: conclusão normal, parada antecipada ou exceção lançada durante a gravação de uma linha. Anteriormente, uma interrupção antecipada ou um erro antes do consumo completo poderia deixar o resultado em aberto e a conexão bloqueada, o que faria com que a próxima consulta nessa solicitação falhasse.

Relatando o intervalo de datas exportado

O cabeçalho de exportação CSV costumava refletir apenas os filtros ativos. Agora, as exportações de Atividade e Tráfego adicionam duas linhas ao cabeçalho, mostrando os registros de data e hora mais antigos e mais recentes abrangidos pelos dados exportados. Como o cabeçalho é gravado antes da primeira linha, o intervalo é obtido por meio de uma consulta MIN / MAX complementar, construída a partir da mesma consulta filtrada da exportação, e é omitido quando nenhuma linha corresponde. Ao arquivar uma exportação, o arquivo agora registra o intervalo de tempo exato que ela representa.

Tornar as falhas de exportação visíveis em vez de silenciosas.

O método de exportação antigo podia falhar silenciosamente. Se o banco de dados estivesse indisponível ou o fluxo de dados não pudesse ser aberto, o código anterior tendia a gerar um arquivo CSV vazio, sem qualquer explicação, o que era o pior cenário possível para alguém tentando recuperar registros durante uma situação de emergência.

Os caminhos de leitura e exportação para ambos os logs foram reformulados para retornar um resultado Revalt que contém os dados ou um erro estruturado, com códigos distintos como activity_export_query_build_failed , activity_export_db_unavailable e activity_export_stream_failed . Uma falha em uma camada inferior é encadeada ao resultado para que a causa raiz original seja preservada em vez de descartada. Falhas de configuração agora encerram a exportação com wp_die() antes que um único byte CSV seja enviado, em vez de transmitir um arquivo vazio.

Quando uma exportação falha, um administrador com a capacidade manage_options vê a causa raiz encadeada, por exemplo, o erro subjacente do banco de dados, anexada à mensagem. Usuários sem essa capacidade não veem, então os detalhes acionáveis chegam às pessoas que podem agir sobre o problema, enquanto os detalhes internos do banco de dados permanecem ocultos para todos os outros. A mensagem é escapada por meio de crb_escape_html() .

Consolidar os registros de atividade e tráfego em classes de domínio.

Grande parte desse ciclo era estrutural. O registro de atividades e o registro de tráfego tinham strings SQL e tratamento de resultados dispersos pelo código do painel e de exportação. Movemos essa lógica para as classes CRB_Activity e CRB_Traffic_Log , de modo que a construção de consultas e a busca de linhas agora são feitas por meio de métodos claros como fetch() e stream_log() em vez de percorrerem o código de apresentação como SQL bruto.

Agora, ambas as classes constroem suas consultas com o construtor de consultas DB Warp obtido por meio de warp_get_db() em vez de concatenar manualmente fragmentos WHERE, JOIN e LIMIT. Isso é importante não apenas pela organização. Roteamento de cada valor de filtro fornecido pelo usuário por meio de uma única camada de escape elimina a antiga combinação de escape manual, $wpdb->prepare() e concatenação entre aspas, que é exatamente o tipo de inconsistência que oculta bugs de injeção. Quando uma camada de banco de dados degradada não consegue construir uma consulta, o código agora falha de forma segura com uma condição de não correspondência, em vez de executar uma consulta sem filtro.

Essas refatorações são internas e não alteram o que as telas exibem, mas são a base que tornou as correções de segurança e confiabilidade acima pequenas, locais e verificáveis.

Foram encontrados dois bugs latentes durante a extração do código de alerta.

Mover o envio de alertas administrativos de CRB_Activity::log() para uma classe dedicada, CRB_Activity_Alerts revelou dois bugs preexistentes causados por um array posicional cujos índices numéricos haviam se desalinhado.

O primeiro problema afetou os links do painel de controle dentro dos e-mails de alerta. O emparelhamento de chave esparsa e valor denso deslocava os valores, de modo que um parâmetro de link como filter_ip podia receber o início de um intervalo de IP em vez do endereço pretendido. O mapeamento dos valores para chaves nomeadas corrigiu o alinhamento, e os links nos e-mails de alerta agora apontam para onde deveriam.

O segundo problema afetava a correspondência de usuários na string de pesquisa. O código chamava wp_get_current_user() , que retornava um objeto WP_User mesmo para o ID de usuário 0, fazendo com que a correspondência usasse a identidade errada. Agora, ele busca o usuário do evento com crb_get_userdata() e evita que o usuário não seja encontrado. Alertas que correspondem a um usuário agora correspondem ao usuário correto.

Um painel de controle mais silencioso: prioridade do operador na verificação de modificações.

CRB_Activity::is_modified_since() comparava um timestamp usando $stamp < $status['data_modified'] ?? PHP_INT_MAX . A precedência de operadores vinculava isso como (... < ...) ?? PHP_INT_MAX , o que tornava a parte de coalescência nula código morto e gerava um aviso de chave indefinida sempre que data_modified estava ausente. A coalescência agora está entre parênteses, então um timestamp de modificação ausente é tratado como "modificado", correspondendo ao método irmão ` is_modified() , e o aviso espúrio desapareceu.

Centralizando as definições de esquema de banco de dados

A última peça estrutural é a manutenção do esquema. O código de instalação e atualização usava SQL CREATE TABLE embutido para as tabelas de log, o que significava que a mesma tabela podia ser descrita em mais de um lugar. Essas declarações agora vêm de uma única fonte, CRB_Schema_Definitions , de modo que a instalação e a atualização se comportem a partir de uma definição canônica e CRB_Schema_Manager possa detectar desvios de esquema em instalações existentes de forma mais consistente. O SQL DROP INDEX bruto também foi substituído por CRB_Schema_Manager::drop_index_if_exists() .

Também tornamos a decodificação de dados armazenados em campos de requisição mais tolerante. Valores legados que podem ser nulos, valores vazios, JSON inválido e payloads serializados não suportados agora são tratados da mesma forma segura, resultando em uma matriz vazia em vez de permitir que um registro malformado interrompa a leitura.

Por que este lançamento é importante?

Não há novos botões na versão 9.8.3. Em vez disso, temos uma camada de dados que falha de forma segura em vez de falhar silenciosamente, e-mails de notificação que não podem ser manipulados por um nome de perfil manipulado, um aviso administrativo que escapa dados externos corretamente, exportações de logs que são executadas em memória plana e informam quando algo dá errado, e filtros de pesquisa que fazem exatamente o que o formulário indica. Esses são os tipos de mudanças que mantêm um plugin de segurança confiável a longo prazo, e preferimos fazer esse trabalho de forma transparente do que fingir que o código anterior já era perfeito.


I'm a team lead in Cerber Tech. I'm a software & database architect, WordPress - PHP - SQL - JavaScript developer. I started coding in 1993 on IBM System/370 (yeah, that was amazing days) and today software engineering at Cerber Tech is how I make my living. I've taught to have high standards for myself as well as using them in developing software solutions.

View Comments
There are currently no comments.