Skip to content

fix(security): reforça sanitização de HTML/CSS contra mXSS e injeção - #143

Merged
ram0ng1 merged 7 commits into
masterfrom
claude/modest-gauss-20h9hw
Jun 11, 2026
Merged

fix(security): reforça sanitização de HTML/CSS contra mXSS e injeção#143
ram0ng1 merged 7 commits into
masterfrom
claude/modest-gauss-20h9hw

Conversation

@ram0ng1

@ram0ng1 ram0ng1 commented Jun 11, 2026

Copy link
Copy Markdown
Owner

Resumo

Endurece as defesas defense-in-depth nos campos de HTML/CSS controlados por administrador que acabam chegando a m.trust() / innerHTML / <style> dos visitantes. Corrige os achados 1, 2 e 3 da revisão de segurança recente. Nenhuma vulnerabilidade era explorável por visitante anônimo ou usuário comum — todas dependem de conteúdo de admin — mas o objetivo declarado do projeto é "comprometimento de conta admin não deve virar XSS para guests".

Mudanças

1. Defesa contra mutation-XSS (mXSS) — HtmlSanitizer (PHP) e sanitizeAdminHtml (JS)

  • Ambos passam a re-executar o scrub até a serialização estabilizar (com teto de 5 passes). Isso pega markup que só se torna perigoso após um ciclo parse → serialize (o navegador re-parseia diferente do DOMDocument/DOMParser).
  • Removem nós de comentário HTML (<!-- ... -->) e os elementos <noscript> / <template> — vetores clássicos de mXSS por mudança de contexto de parser.

2. Corpo de <style> higienizado no sanitizador JS (corrige inconsistência com o PHP)

  • Antes, sanitizeAdminHtml não inspecionava <style>, então o campo de rodapé (footerHtml, içado para o <head>) podia carregar @import url(//evil)/expression()/url(data:) etc. servidos a todos os visitantes (exfiltração/injeção via CSS).
  • Agora mantém CSS legítimo (animações) e descarta o <style> quando ele contém um sink de CSS.

3. Injeção de CSS via cor de grupo na TeamPage

  • TeamPage.tsx interpolava role.color() cru em style=. Agora valida via novo helper safeCssColor() (espelha o safeColor() do PHP usado no spinner); valor inválido/forjado cai para "sem background inline" em vez de injetar declarações CSS.

Testes

  • tests/Unit/HtmlSanitizerTest.php: novos casos para strip de comentários, <noscript>/<template> e idempotência do sanitizador (o loop anti-mXSS).
  • composer test → 80 testes OK · composer analyse (phpstan) → sem erros · tsc --noEmit → OK · npm run build (lint + webpack) → OK.
  • Bundles em js/dist recompilados (o extend.php carrega o bundle compilado).
  • Matriz tests/SECURITY_COVERAGE.md (§9 / §9.3) atualizada.

Fora de escopo

  • Migração para allowlist real (HTMLPurifier / DOMPurify) — seria a defesa definitiva, mas adiciona dependência/peso de bundle; este PR endurece os denylists existentes sem novas dependências.
  • Content-Security-Policy / nonce — alteração arquitetural arriscada (a extensão emite scripts inline no <head>); fica como observação.

https://claude.ai/code/session_01THwoXq4vQ9zJphMCPBS97m


Generated by Claude Code

Endurece as defesas de defense-in-depth nos campos de HTML/CSS controlados
por admin que chegam a m.trust()/innerHTML/<style> dos visitantes.

- HtmlSanitizer (PHP) e sanitizeAdminHtml (JS): re-executam o scrub até a
  serialização estabilizar (defesa contra mutation-XSS via diferenças de
  parsing), removem nós de comentário e os elementos <noscript>/<template>
  (vetores clássicos de mXSS por mudança de contexto de parser).
- sanitizeAdminHtml (JS): passa a higienizar o corpo de <style> (mantém CSS
  de animação, descarta @import/expression()/behavior/-moz-binding/url(data:)
  /esquemas de script). Antes o <style> do rodapé chegava sem inspeção aos
  visitantes, permitindo injeção/exfiltração via CSS.
- TeamPage: valida a cor do grupo com o novo safeCssColor() antes de
  interpolar no atributo style=, evitando injeção de declarações CSS.
- Testes: cobre strip de comentários, <noscript>/<template> e idempotência
  do sanitizador; atualiza a matriz SECURITY_COVERAGE.md.
- Recompila os bundles em js/dist.

https://claude.ai/code/session_01THwoXq4vQ9zJphMCPBS97m
@github-actions

github-actions Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

📦 Bundle size

🟡 Crescimento pequeno — gzip total +0.3 KB (+0.5%), abaixo do limiar de regressão (2% / 2 KB).

Arquivo PR (gzip) Master (gzip) Δ gzip PR (raw) Δ raw
admin.js 11.3 KB 11.3 KB = 45.2 KB =
forum.js 33.9 KB 33.6 KB 🔴 +0.3 KB (0.9%) 133.4 KB 🔴 +0.6 KB (0.5%)
forum/components/AllDiscussionsPage.js 2.7 KB 2.7 KB = 8.7 KB =
forum/components/AvocadoPostsSearchPage.js 2.4 KB 2.4 KB = 7.3 KB =
forum/components/AvocadoSearchPage.js 5.3 KB 5.3 KB = 19.5 KB =
forum/components/TagPage.js 3.4 KB 3.4 KB = 11.0 KB =
forum/components/TeamPage.js 1.4 KB 1.4 KB 🔴 +0.0 KB (1.0%) 3.8 KB 🔴 +0.0 KB (0.8%)
forum/components/UserProfilePage.js 2.5 KB 2.5 KB = 10.2 KB =
Total 62.8 KB 62.5 KB 🔴 +0.3 KB (0.5%) 239.0 KB 🔴 +0.7 KB (0.3%)

gzip nível 9. 🟢 = menor que o master · 🔴 = maior. Report-only — não bloqueia o merge.

@github-actions

github-actions Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

🔒 Regressão de segurança (Semgrep diff vs master)

✅ Sem regressão — nenhum achado novo de vulnerabilidade introduzido por esta PR.

@github-actions

github-actions Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

🔬 Performance benchmark

Home (/)

Métrica PR Master (baseline) Δ
Performance (score) 81 82 🔴 -1pp (-1.2%)
FCP 1853 ms 1707 ms 🔴 +145 ms (8.5%)
LCP 2056 ms 1951 ms 🔴 +105 ms (5.4%)
TBT 60 ms 100 ms 🟢 -40 ms (-40.0%)
CLS 0.000 0.000 (=)
Speed Index 1853 ms 1707 ms 🔴 +145 ms (8.5%)
TTI 2056 ms 1951 ms 🔴 +105 ms (5.4%)

💡 Recomendações (Lighthouse + dicas Flarum 2 / Avocado)

  • Sem compressão de texto — economia ~880 ms · ~1099 KB
    Habilite gzip/brotli no servidor (nginx: gzip on; gzip_types text/css application/javascript; ou brotli on; brotli_types ...). Esse é um setting de host, não da extensão, mas reportar aqui ajuda.
  • JavaScript não utilizado — economia ~640 ms · ~516 KB
    Use import() dinâmico para componentes só usados em rotas específicas (TeamPage, AvocadoSearchPage). O webpack faz split automático se você usar import("./components/TeamPage") dentro do routes ao invés de import estático no topo do index.tsx.
  • CSS não utilizado — economia ~480 ms · ~538 KB
    O bundle forum.css carrega tudo de less/forum/**. Divida por rota (Home/Discussion/Tag) via Extend\Frontend->css() condicional no JS, ou use PurgeCSS no webpack para o build de produção.
  • Recursos bloqueando o render — economia ~400 ms
    Mova CSS não-crítico para <link rel="preload"> ou injete inline o CSS above-the-fold do forum.less. Em Flarum 2, o forum.css é servido como bloqueante por padrão — considere media="print" onload para folhas não-críticas (ex.: dark theme).
  • Cache HTTP curto — 8 resources found
    Os assets versionados em /assets/forum-<hash>.js deveriam ter Cache-Control: public, max-age=31536000, immutable. Configure no nginx/apache, não no Flarum.

/discussions

Métrica PR Master (baseline) Δ
Performance (score) 87 87 (=)
FCP 1445 ms 1403 ms 🔴 +42 ms (3.0%)
LCP 1790 ms 1788 ms 🔴 +2 ms (0.1%)
TBT 6 ms 7 ms 🟢 -1 ms (-14.3%)
CLS 0.000 0.000 (=)
Speed Index 1445 ms 1403 ms 🔴 +42 ms (3.0%)
TTI 1790 ms 1788 ms 🔴 +2 ms (0.1%)

💡 Recomendações (Lighthouse + dicas Flarum 2 / Avocado)

  • Sem compressão de texto — economia ~920 ms · ~1105 KB
    Habilite gzip/brotli no servidor (nginx: gzip on; gzip_types text/css application/javascript; ou brotli on; brotli_types ...). Esse é um setting de host, não da extensão, mas reportar aqui ajuda.
  • JavaScript não utilizado — economia ~550 ms · ~538 KB
    Use import() dinâmico para componentes só usados em rotas específicas (TeamPage, AvocadoSearchPage). O webpack faz split automático se você usar import("./components/TeamPage") dentro do routes ao invés de import estático no topo do index.tsx.
  • Recursos bloqueando o render — economia ~520 ms
    Mova CSS não-crítico para <link rel="preload"> ou injete inline o CSS above-the-fold do forum.less. Em Flarum 2, o forum.css é servido como bloqueante por padrão — considere media="print" onload para folhas não-críticas (ex.: dark theme).
  • CSS não utilizado — economia ~480 ms · ~544 KB
    O bundle forum.css carrega tudo de less/forum/**. Divida por rota (Home/Discussion/Tag) via Extend\Frontend->css() condicional no JS, ou use PurgeCSS no webpack para o build de produção.
  • Cache HTTP curto — 9 resources found
    Os assets versionados em /assets/forum-<hash>.js deveriam ter Cache-Control: public, max-age=31536000, immutable. Configure no nginx/apache, não no Flarum.

/tags

Métrica PR Master (baseline) Δ
Performance (score) 87 88 🔴 -1pp (-1.1%)
FCP 1447 ms 1485 ms 🟢 -37 ms (-2.5%)
LCP 1747 ms 1688 ms 🔴 +59 ms (3.5%)
TBT 0 ms 0 ms (=)
CLS 0.000 0.000 (=)
Speed Index 1447 ms 1485 ms 🟢 -37 ms (-2.5%)
TTI 1747 ms 1688 ms 🔴 +59 ms (3.5%)

💡 Recomendações (Lighthouse + dicas Flarum 2 / Avocado)

  • Sem compressão de texto — economia ~920 ms · ~1100 KB
    Habilite gzip/brotli no servidor (nginx: gzip on; gzip_types text/css application/javascript; ou brotli on; brotli_types ...). Esse é um setting de host, não da extensão, mas reportar aqui ajuda.
  • Recursos bloqueando o render — economia ~520 ms
    Mova CSS não-crítico para <link rel="preload"> ou injete inline o CSS above-the-fold do forum.less. Em Flarum 2, o forum.css é servido como bloqueante por padrão — considere media="print" onload para folhas não-críticas (ex.: dark theme).
  • JavaScript não utilizado — economia ~500 ms · ~541 KB
    Use import() dinâmico para componentes só usados em rotas específicas (TeamPage, AvocadoSearchPage). O webpack faz split automático se você usar import("./components/TeamPage") dentro do routes ao invés de import estático no topo do index.tsx.
  • CSS não utilizado — economia ~480 ms · ~543 KB
    O bundle forum.css carrega tudo de less/forum/**. Divida por rota (Home/Discussion/Tag) via Extend\Frontend->css() condicional no JS, ou use PurgeCSS no webpack para o build de produção.
  • Cache HTTP curto — 8 resources found
    Os assets versionados em /assets/forum-<hash>.js deveriam ter Cache-Control: public, max-age=31536000, immutable. Configure no nginx/apache, não no Flarum.

🥑 Dicas gerais para acelerar o tema (Flarum 2 + Avocado)

  1. Build de produção minificado — confirme que npm run build rodou com mode: production (já é o caso no js/package.json). Webpack tree-shakes import { x } from "flarum/..." se o consumo for explícito.
  2. Split por rotaindex.tsx importa todos os Components no topo. Trocar para import() dinâmico nas rotas raramente acessadas (TeamPage, AvocadoSearchPage, TagsPage) reduz o forum.js inicial.
  3. Less crítico inline — Flarum 2 serve forum.css bloqueando. Considere extrair o CSS above-the-fold (header + 1ª thread visível) e injetar inline via Extend\Frontend->content(InlineCriticalCss::class).
  4. Avatares — sirva via <img loading="lazy"> em todos os ThreadCards/PostCards que não estiverem no fold inicial.
  5. <link rel="preconnect"> — se você usa CDN para fontes ou avatares S3, adicione preconnects no header.
  6. Extend\Frontend->js() rodam síncronos — todo arquivo js/dist/forum.js é parseado no boot. Cada extend() no index.tsx roda antes da primeira pintura.
  7. opcache + view cache — no host: opcache.enable=1, opcache.validate_timestamps=0 em produção, e php flarum cache:clear no deploy.
  8. HTTP/2 + Brotli no host — o Flarum gera bundles grandes; sem brotli você paga em transferência.

Lighthouse desktop, 1 run por URL. 🟢 = melhorou vs master · 🔴 = regrediu.

github-actions Bot and others added 5 commits June 11, 2026 12:16
… phpunit

O bump do phpunit para ^13.2.0 (Dependabot #141) quebrou a CI no master:

1. phpunit 13 exige PHP >= 8.4.1, mas flarum/core 2.x suporta ^8.3 e a matriz
   testa 8.3 → `composer update` falhava em todos os cells/jobs de PHP 8.3
   (o --ignore-platform-req=php+ só ignora limites superiores, não inferiores).
   Constraint união `^11.5 || ^13.2.0`: 8.3 resolve phpunit 11.5, 8.4+ resolve 13.

2. `--filter sqlite` casava o data set no phpunit 11 mas não no 13, que então
   reportava "No tests executed" e saía com código 1, derrubando os steps de
   integração. Troca para a sintaxe de data set `--filter '#sqlite'` (idem
   mysql/pgsql), que funciona em ambas as versões.

Validado local: composer test (80), phpstan (sem erros), integração #sqlite OK.

https://claude.ai/code/session_01THwoXq4vQ9zJphMCPBS97m
…ente

Com a estratégia padrão, o Dependabot não abre PR para releases do Flarum já
cobertas pela faixa `^2.0.0` (só alargaria o range). `versioning-strategy:
increase` faz ele bumpar o piso da constraint quando sai versão nova (incl.
prereleases rc/beta, já que composer.json tem minimum-stability: dev), então
novas versões do core passam a ser propostas e testadas pela CI automaticamente.

- groups.flarum: consolida flarum/core + flarum/tags num único PR por ciclo.
- ignore phpunit major: protege a constraint união ^11.5 || ^13.2.0 de ser
  revertida para 13-only (que quebraria os cells de PHP 8.3).

https://claude.ai/code/session_01THwoXq4vQ9zJphMCPBS97m
@ram0ng1 ram0ng1 added melhoria Nova funcionalidade ou aprimoramento (minor) release labels Jun 11, 2026
@ram0ng1
ram0ng1 merged commit 6825d71 into master Jun 11, 2026
23 checks passed
ram0ng1 added a commit that referenced this pull request Jun 11, 2026
fix(security): reforça sanitização de HTML/CSS contra mXSS e injeção
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

melhoria Nova funcionalidade ou aprimoramento (minor)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants