feature: XSS e padronização em echo de PHP - #2821
Merged
Merged
Conversation
…ss:check As views exibiam linhas do banco, valores de sessão, valores de configuração e parâmetros de query string sem escape. Apenas 51 das ~2090 expressões de saída eram escapadas. Adiciona escapadores por contexto em general_helper.php: esc() texto HTML e atributos entre aspas esc_js() literais JavaScript dentro de <script> esc_json() arrays e estruturas dentro de <script> esc_url() href/src, rejeita javascript: e data: esc_css() valores dentro de um atributo style esc_msg() flashdata exibido em popup do SweetAlert Correções críticas: - relatorios/imprimir/imprimirEtiquetas.php refletia etiquetaCode diretamente em um atributo <barcode type="...">; agora é validado contra uma whitelist que espelha produtos.php, com EAN13 como padrão. - O flashdata era embutido em <script> em tema/conteudo.php e nas views conecte/, então qualquer mensagem virava código executável. Agora passa por esc_msg(), que remove as tags e codifica o valor como JSON antes de ir para o swal() da biblioteca carregada no layout. - Links de WhatsApp em os/visualizarOs.php e os/editarOs.php agora aplicam rawurlencode() no telefone e na mensagem e passam por esc_url(). As demais saídas sem escape foram corrigidas em 63 views. Valores que carregam markup pré-renderizado de propósito ($topo, $custom_error e o retorno de printSafeHtml()) ficam sem escape. esc_url() antes retornava string vazia para caminhos relativos simples, quebrando silenciosamente qualquer link que a usasse, e aceitava esquemas contrabandados com caracteres de controle; os dois casos foram tratados. Corrige ainda oito literais JavaScript que estavam sendo escapados com esc(), que é o escapador de HTML. Eles fechavam a string com aspas manuais, o que neutraliza as aspas, mas não a barra invertida nem a quebra de linha, então um valor desses derrubaria o script. Agora passam por esc_js(), que já devolve o valor entre aspas, seguindo a convenção já usada em tema/rodape.php e nas duas views de venda. Adiciona tools/check_view_escaping.php com uma baseline revisada, ligado como `composer xss:check` e executado na CI via .github/workflows/quality.yml. O verificador foi testado para detectar propriedades soltas, concatenação, ramos de ternário sem escape, valores de sessão e acesso a array. O verificador também ganhou uma segunda checagem, sobre a forma do valor e não apenas sobre o escape. Ela acusa um JSON.parse() alimentado por um escapador: esc_json() e esc_js() emitem um valor JSON solto, sem aspas, e esc() emite entidades HTML, então nenhum dos três consegue produzir a string que o JSON.parse() espera. Isso atingia cobrancas/modalGerarPagamento.php, onde a config dos gateways deixava de ser atribuída. O valor passa a ser atribuído direto, e a regra impede a reincidência. Havia ali um segundo defeito, independente, e que era a causa real do select de forma de pagamento vazio. $modalGerarPagamento vem de $this->load->view(..., true), ou seja, a view já vem renderizada como string HTML. Escapá-la com esc() não é redundante, é destrutivo: o htmlspecialchars() transformava o modal inteiro em texto de entidades e neutralizava o <script> interno, de modo que paymentGatewaysConfig nunca chegava a ser definida e script-payments.js não populava o select. Os dois defeitos precisavam ser corrigidos para o fluxo funcionar. As duas views que consomem o valor, os/visualizarOs.php e vendas/visualizarVenda.php, voltam a emiti-lo cru, como antes. Por isso o verificador tem uma terceira checagem, o inverso da regra que aceita esses valores crus: acusa um escapador envolvendo uma variável que carrega markup pré-renderizado. A lista é uma só, compartilhada entre as duas regras, para não divergirem. A checagem é ancorada no escapador envolvendo a variável diretamente, então esc($result->idOs) não é acusado; a limitação é não enxergar markup numa variável fora da lista, nem escondido atrás de uma chamada como trim(). Também corrige .php-cs-fixer.php, que recursava no mount MySQL docker/data e abortava, fazendo `composer format` não verificar nada.
Substitui `<?php echo EXPR; ?>` e `<?php print(EXPR); ?>` avulsos por `<?= EXPR ?>` nas views e acrescenta os espaços que faltavam nas tags curtas `<?=EXPR?>` existentes. Puramente sintático. Blocos que misturam controle de fluxo com saída (como `if` com echo, ou blocos com várias instruções) continuam com `<?php`, assim como as três views de erro CLI em PHP puro que usam `echo` com múltiplos argumentos. Verificado com uma impressão digital de nível de token de cada view alterada: a sequência de expressões emitidas e de HTML inline é byte-idêntica à anterior, e a contagem de linhas e de linhas em branco não mudou.
Pr3d4dor
force-pushed
the
feature/xss-and-style
branch
from
September 26, 2026 21:37
2d32de2 to
a3beba7
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Descrição
Este PR tem dois commits independentes.
1.
fix— Escape da saída das views por contextoAs views imprimiam direto na tela, sem escape, linhas do banco, valores de
sessão, valores de configuração e parâmetros de query string. Em uma auditoria
de ~2090 expressões de saída, apenas 51 estavam escapadas.
O PR adiciona escapadores por contexto em
application/helpers/general_helper.phpe aplica o escaper correto em cadasituação:
esc()esc_js()<script>esc_json()<script>esc_url()href/src, rejeitandojavascript:edata:esc_css()styleesc_msg()Três problemas com impacto real foram corrigidos:
Atributo
<barcode type="...">—relatorios/imprimir/imprimirEtiquetas.phprefletia
etiquetaCodesem validação. Agora passa por uma whitelist queespelha
produtos.php, comEAN13como padrão.Flashdata dentro de
<script>— emtema/conteudo.phpe nas viewsconecte/, qualquer mensagem de flashdata virava JavaScript executável: ocódigo usava
str_replace('"', '', $var), contornável com</script>. Agorao valor passa por
esc_msg(), que remove as tags e codifica o texto comoJSON antes de ir para o
swal()da biblioteca carregada no layout.Links de WhatsApp —
os/visualizarOs.phpeos/editarOs.phpagora aplicamrawurlencode()no telefone e na mensagem e passam poresc_url().Markup pré-renderizado escapado como valor comum —
cobrancas/modalGerarPagamento.phpé carregado com
$this->load->view(..., true), ou seja, a view já chegarenderizada como string HTML. Em
os/visualizarOs.phpevendas/visualizarVenda.phpesse valor passou poresc(), o que não éredundante, é destrutivo:
htmlspecialchars()transformava o modal inteiroem texto de entidades e neutralizava o
<script>interno, de modo quepaymentGatewaysConfignunca era definido e o select Forma de Pagamentonascia vazio, porque
assets/js/script-payments.js:46depende dele. As duasviews voltaram a emiti-lo cru, como antes.
JSON.parse()alimentado por um escapador — o mesmo modal embutia aconfig dos gateways com
JSON.parse(<?= esc_json(...) ?>). Comoesc_json()já emite um valor JSON solto, sem aspas, o
JSON.parse()recebia um objeto,era coagido para
"[object Object]"e lançariaSyntaxError. O valor passoua ser atribuído direto. Isso também fecha um furo anterior: o código antigo
usava
addslashes()dentro de aspas, contornável com</script>.Este e o item acima eram dois defeitos independentes no mesmo fluxo, e os
dois precisavam ser corrigidos. Corrigir só o
JSON.parsenão resolvia oselect vazio, porque o
<script>que o executava nem chegava ao navegador.Oito literais JavaScript escapados com
esc()—esc()é o escapador deHTML e não neutraliza barra invertida nem quebra de linha, então um valor com
esses caracteres derrubaria o script. O fechamento por aspas manuais mantinha
o valor inerte na prática, mas por acidente. Agora passam por
esc_js(), quejá devolve o valor entre aspas, seguindo a convenção já usada em
tema/rodape.phpe nas duas views de venda. Conferido queesc_js()converteescalares para string, de modo que
controlBaixa === '1'emfinanceiro/lancamentos.php:938continua valendo.Além disso,
esc_url()era corrigido: antes retornava string vazia paracaminhos relativos simples (quebrav silenciosamente qualquer link que a
usasse) e aceitava esquemas escondidos atrás de caracteres de controle.
O restante das saídas sem escape foi tratado em 63 views. Valores que
carregam markup pré-renderizado de propósito (
$topo,$custom_errore oretorno de
printSafeHtml()) ficam sem escape, por design.Para evitar regressão, o PR adiciona
tools/check_view_escaping.php, ligadocomo
composer xss:checke executado na CI via.github/workflows/quality.yml. O verificador roda três checagens independentes:escape, valores de sessão e acesso a array.
JSON.parse()acima. Nenhum dos três escapadores (esc,esc_js,esc_json) consegue produzir a string que oJSON.parse()espera, então aregra acusa o padrão e orienta a atribuição direta. A forma canônica
JSON.parse(<?= esc_js(json_encode($x)) ?>)continua válida e não é acusa.carrega HTML finalizado. É o inverso da regra que aceita esses valores crus,
e existe porque o defeito do modal passou justamente por esse ponto: o
valor era aceito cru e foi escapado depois. As duas regras leem a mesma lista
$preRendered, para não divergirem.As três rodam com baseline revisada e vazia (zero tolerância) e relatam em
seções separadas, porque são motivos de falha diferentes.
Vale registrar por que as checagens 2 e 3 existem: as regressões do
JSON.parse(), doSwal is not definede do modal de pagamento atravessaramphp -l,composer xss:checke a verificação de neutralidade do commit deestilo sem serem detectadas. A primeira era visível só no console do navegador;
a segunda só porque alguém testou a tela.
O commit também corrige
.php-cs-fixer.php, que recursava no mount MySQLdocker/datae abortava — fazendocomposer formatnão verificar nada.2.
style— Tags curtas de echo na saída das views<?php echo EXPR; ?>e<?php print(EXPR); ?>avulsos foram substituídos por<?= EXPR ?>, e as tags curtas<?=EXPR?>que existiam sem espaço ganharamespaçamento. São 1172 conversões e 22 tags respaçadas em 89 views.
Puramente sintático: blocos que misturam controle de fluxo com saída (como
ifcomecho, ou blocos com várias instruções) continuam com<?php, assimcomo as três views de erro CLI em PHP puro que usam
echocom múltiplosargumentos.
Issue relacionada
Em branco. Não há issue pública — a auditoria de escape não foi aberta como
issue, e por se tratar de hardening de segurança o detalhamento foi mantido
no corpo dos commits.
Tipo de mudança
fix)feat)docs)refactor)chore)Como testar
Verificação automatizada (executada, tudo passando):
Para o revisor confirmar que o verificador pega as três classes de regressão:
Todas as três formas foram exercitadas contra o verificador durante a revisão,
e o caso 3 foi confirmado de ponta a ponta em
os/visualizarOs.php. No mesmoteste, a regra de markup pré-renderizado foi verificada em 15 casos: acusa
esc(),esc_html()ehtmlspecialchars()sobre$topo,$custom_errore$modalGerarPagamento, e não acusaesc($result->idOs),esc($qrCode),esc($whatsappUrl), nem nomes parecidos como$topoExtra. A limitaçãoconhecida é não enxergar markup escondido atrás de uma chamada como
esc(trim($modalGerarPagamento)).Além disso, a saída do
esc_json()foi validada em runtime contra a estruturareal de
application/config/payment_gateways.php: o JSON é válido,payment_methodssai como array JSON com os métodos na ordem, etimeouteproductionmantêm seus tipos.Para o commit de estilo, a prova de que nada mudou visualmente é o
git diffser estritamente linha a linha (2282 inserções / 1511 remoções nototal, sendo 1156/1156 o commit de estilo) e a conferência de que a indentação e a
estrutura de linhas não mudaram em nenhuma das 89 views. A neutralidade também
foi conferida por impressão digital dos tokens: as 89 views produzem o mesmo
resultado antes e depois do commit de estilo.
Regressão do modal de pagamento (prioridade máxima): o select Forma de
Pagamento nascia vazio, por dois defeitos independentes que precisaram ser
corrigidos juntos. Este é o fluxo que valida a página:
Cobranças > Gerar pagamentonuma OS e numa Venda — o modal precisaaparecer como modal, e não como texto de marcação.
ser populado com Boleto e Link.
acompanha a troca.
SyntaxError, ewindow.paymentGatewaysConfigdeve ser um objeto com a chave do gateway.OS > Visualizare emVendas > Visualizar, que são as duas viewsque consomem o valor pré-renderizado.
Regressão do flashdata: as views
tema/conteudo.phpeconecte/template.phpexibem o toast de flashdata. O projeto carrega duasbibliotecas SweetAlert —
assets/js/sweetalert.min.js(v1, expõeswal()),carregada globalmente em
tema/topo.php:42, eassets/js/sweetalert2.all.min.js(v2, expõeSwal), carregada por página.Qualquer chamada a
Swalnuma página que não carrega a v2 quebra comReferenceError: Swal is not definede nenhum aviso de sucesso aparece:Produtos > Editar, altere o preço e salve — deve aparecer o toast verdede sucesso, e o console não deve mostrar erro.
aparecer o toast vermelho.
/mine), repita: solicitar alteração de senha econfirmar o toast.
ReferenceError.Regressão de
esc_js()(8 literais): confirme que o valor chega íntegro eque nenhum
SyntaxErroraparece no console.Baixa" das configurações: com ele ativo, o filtro de vencimento continua
liberado.
excluir anotação dependem todos de
idOS. Os quatro modais devem abrir e oregistro correto ser alterado.
envio acontece (o
tokené o valor movido).Validação manual recomendada nas telas tocadas:
imprimirOs,imprimirOsTermica) e conferir o campo de status (usa ternários).editarVenda(desconto em percentual), imprimir viaimprimirVenda/imprimirVendaOrcamento/imprimirVendaTermica.clientes,editarCliente,visualizar.conecte/) e envio do e-mail de nova senha.código de barras e o SKU continuam impressos corretamente.
lancamentose os relatórios (revisar os totais, que usamternários com cast).
aqui está coberto pelo teste de regressão de prioridade máxima acima).
Também os relatórios de garantia.
logs, incluindo a paginação e o modal de exclusão.configurar,emitente, painel e busca.permissoeseeditarPermissao(blocos comifqueficaram em
<?php).relatorios/imprimir/*: etiquetas de produtos,OS, vendas, clientes, financeiro, SKU, serviços.
errors/html/*eerrors/cli/*.Capturas de tela
Obrigatórias. O escape muda o que é renderizado em pontos onde antes havia
HTML injetado, então é preciso comparar antes e depois das telas acima — em
especial:
imprimirEtiquetas) — otypedo barcode agora évalidado por whitelist
conecte/, antes vulneráveis a execução via flashdataprintSafeHtml()continua renderizandorich text
Checklist
composer format..env, credenciais, dumps de banco ou arquivos de IDE no diff.application/vendor/não foi commitada.embutidos em JavaScript.