Files
Claude Code 4a55d51329 feat(wordpress): EMCP Tools workflow skills + widget catalogs + pending wordpress skills
- emcp-page-building, emcp-content-ops, emcp-site-audit: EMCP Tools MCP
  workflows verified live (atomic/legacy interplay, apply-template overwrite
  risk, create-theme-template goes live immediately, false-positive malware
  pattern in scan-security, change ledger + rollback)
- elementor-pro-widgets: 30+5 native Elementor Pro widgets (curated catalog)
- elementskit-widgets / powerpack-widgets: 42 + 97 third-party widgets,
  widgetType extracted from plugin source (not guessed by convention)
- plugin.json bumped 1.2.0 -> 1.3.0, keywords + description updated
- commits pending wordpress skills already present as untracked files
  (emcp-tools, wordfence, wp-activity-log, seguranca-descomplicar,
  webp-express, wp-fastest-cache, wp-font-perf, wp-meteor,
  wp-activity-log, redis-object-cache, app-for-cloudflare) and pending
  edits (rank-math, wp-cli, wp-content-seo-gate)
2026-08-19 03:44:00 +01:00

739 lines
42 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: wordfence
description: Auditoria e gestão completa do Wordfence Security via SQL directo em servidores CWP — cobre as 313 chaves de `wfconfig`, a tabela separada `wfls_settings` do módulo Login Security (2FA/passkeys/CAPTCHA/XML-RPC), armazenamento do WAF em ficheiros (`wp-content/wflogs/config*.php`), WAF (modos, categorias de regra, blacklist premium), bloqueio de IPs/países/padrões (UA/referrer/hostname), Live Traffic, os 5 limitadores de Rate Limiting, Diagnostics, Import/Export de config e Wordfence Central. Detecção de IP real atrás do Cloudflare (howGetIPs), scan settings e interacção com plugins de cache. Usar quando "wordfence", "waf wordpress", "firewall wordpress", "2fa wordpress", "passkeys wordpress", "bloqueio de login", "wfconfig", "wfls_settings", "cloudflare real ip wordfence", "howGetIPs", "scan malware wordpress", "brute force wordpress", "rate limiting wordpress", "live traffic wordfence", "wordfence central", "bloqueio de país wordpress".
---
# /wordfence — Auditoria e Configuração Wordfence via SQL
Gestão do Wordfence Security via `wp db query` directo no servidor CWP.
**A configuração NÃO vive toda no mesmo sítio:**
- Grosso da config (WAF nível aplicação, scan, alertas, blocking,
rate limiting) → tabela própria `wfconfig` (chave/valor, colunas
`name`/`val`), **não** `wp_options` — `wp option get/patch` não
funciona para nada disto.
- 2FA, passkeys, CAPTCHA de login, XML-RPC, "remember device" (módulo
Login Security) → tabela **separada** `wfls_settings` (colunas
`name`/`value` — nomes de coluna diferentes de `wfconfig`). Ver §6.
- Config do motor WAF usada para arrancar antes do WordPress (Extended
Protection/auto-prepend) → **ficheiros PHP serializados** em
`wp-content/wflogs/config*.php`, fora de qualquer tabela SQL,
sincronizados automaticamente a partir de `wfconfig`. Ver §10.
**Fonte:** `CONFIG-Plugins-Referencia.md` §8 (Wordfence, mapeado
16-08-2026, site piloto `emanuelalmeida.pt`, versão `9.0.0` gratuita) +
leitura completa do código-fonte do plugin (466 ficheiros PHP) e
verificação SQL/SSH ao vivo feitas nesta sessão — detalhe completo da
cobertura no rodapé "Fonte" no final do documento.
---
## Contexto CWP — sempre obrigatório
```bash
PATH=/home/USER/public_html
PREFIX=$(wp db prefix --allow-root --path=$PATH) # ex.: wpne_, wpah_ — nunca assumir wp_
```
**Acesso via SSH:** `server.descomplicar.pt`, porta `9443`, user `root`.
Servidor executa comandos como o utilizador do site: `sudo -u USER wp ... --path=$PATH`.
**Tabela de config real (schema confirmado):**
```
DESCRIBE ${PREFIX}wfconfig;
name varchar(100) PRI
val longblob -- valor (string, número ou JSON serializado)
autoload enum('no','yes')
```
Não há `wp option patch` equivalente — é sempre `SELECT`/`UPDATE` directo
sobre `name`/`val`. **313 linhas** de config no site piloto.
---
## Decision tree — qual comando usar
| Operação | Comando |
|---|---|
| Ler 1 chave de config | `wp db query "SELECT name, val FROM ${PREFIX}wfconfig WHERE name='...';"` |
| Ler várias chaves | `SELECT name, val FROM ${PREFIX}wfconfig WHERE name IN (...);` |
| Alterar 1 chave (produção) | `UPDATE ${PREFIX}wfconfig SET val='...' WHERE name='...';` — **preferir sempre a UI do wp-admin** para chaves que disparam hooks (`wordfence_changed_ip_source`, `wordfence_changed_license_key`) |
| Contar utilizadores com 2FA | `SELECT COUNT(*) FROM ${PREFIX}wfls_2fa_secrets;` (tabela moderna) — **não** `usermeta` (legado, pode ficar desactualizado) |
| Ver todas as tabelas do plugin | `SHOW TABLES LIKE '${PREFIX}wf%';` |
| Ver issues de scan pendentes | `SELECT COUNT(*) FROM ${PREFIX}wfpendingissues;` |
| Ver IPs bloqueados pelo WAF | `SELECT COUNT(*) FROM ${PREFIX}wfblockediplog;` |
| Confirmar recomendação de proxy do próprio Wordfence | `SELECT val FROM ${PREFIX}wfconfig WHERE name='detectProxyRecommendation';` |
| Ler settings do módulo Login Security (2FA/passkeys/CAPTCHA) | `SELECT name, value FROM ${PREFIX}wfls_settings ORDER BY name;` — **tabela diferente**, colunas `name`/`value` (não `val`) — ver §6 |
| Ver ficheiros de config do WAF (bootstrap fora do WordPress) | `cat wp-content/wflogs/config.php` / `config-synced.php` — **não é SQL**, é PHP serializado com `__halt_compiler()` — ver §10 |
---
## 1. Auditoria de estado (leitura, sem risco)
```bash
PATH=/home/USER/public_html
PREFIX=$(wp db prefix --allow-root --path=$PATH)
# Estado geral do WAF/firewall
wp db query "SELECT name, val FROM ${PREFIX}wfconfig WHERE name IN
('firewallEnabled','waf_status','disableWAFIPBlocking','isPaid');" --allow-root --path=$PATH
# Login security (rate-limit + 2FA)
wp db query "SELECT name, val FROM ${PREFIX}wfconfig WHERE name IN
('loginSec_maxFailures','loginSec_countFailMins','loginSec_lockoutMins',
'loginSec_lockInvalidUsers','allowLegacy2FA','loginSec_requireAdminTwoFactor');" --allow-root --path=$PATH
# Quantos utilizadores TÊM 2FA activo de facto (tabela moderna do módulo login-security)
wp db query "SELECT COUNT(*) AS total_2fa FROM ${PREFIX}wfls_2fa_secrets;" --allow-root --path=$PATH
# Detecção de IP real (crítico atrás de proxy/CDN)
wp db query "SELECT name, val FROM ${PREFIX}wfconfig WHERE name IN
('howGetIPs','howGetIPs_trusted_proxies','howGetIPs_trusted_proxy_preset',
'detectProxyRecommendation');" --allow-root --path=$PATH
# Scans activos
wp db query "SELECT name, val FROM ${PREFIX}wfconfig WHERE name LIKE 'scansEnabled_%';" --allow-root --path=$PATH
# Alertas/email
wp db query "SELECT name, val FROM ${PREFIX}wfconfig WHERE name IN
('alertEmails','email_summary_enabled','liveTrafficEnabled','blockFakeBots','autoUpdate','cacheType');" --allow-root --path=$PATH
# Volume de actividade (contexto do site)
wp db query "SELECT COUNT(*) FROM ${PREFIX}wfblockediplog;" --allow-root --path=$PATH # IPs bloqueados pelo WAF
wp db query "SELECT COUNT(*) FROM ${PREFIX}wfpendingissues;" --allow-root --path=$PATH # issues de scan em aberto
wp db query "SHOW TABLES LIKE '${PREFIX}wf%';" --allow-root --path=$PATH # inventário de tabelas do plugin
```
### Valores reais confirmados (`emanuelalmeida.pt`, gratuito, 16-08-2026)
| Chave | Valor | Avaliação |
|---|---|---|
| `firewallEnabled` | `1` | ✅ |
| `waf_status` | `enabled` | ✅ WAF a funcionar |
| `disableWAFIPBlocking` | `0` | ✅ bloqueio de IP activo (325 IPs bloqueados no log) |
| `isPaid` | vazio | Versão **gratuita** — sem WAF em tempo real (updates de regras com atraso), sem scan de reputação de IP premium |
| `alertEmails` | `it@descomplicar.pt` | ✅ |
| `email_summary_enabled` | `1` | ✅ |
| `liveTrafficEnabled` | `1` | ✅ |
| `blockFakeBots` | `1` | ✅ bloqueia falsos Googlebot/etc. |
| `autoUpdate` | `1` | ✅ |
| `cacheType` | `disabled` | ✅ **deliberado** — ver §4 |
| `scansEnabled_core/plugins/themes/malware/diskSpace/suspiciousAdminUsers/wafStatus/...` | todos `1` | ✅ cobertura de scan quase completa |
| `scansEnabled_highSense` | `0` | Sensibilidade alta de scan desligada — reduz falsos positivos, aceitável |
| `scansEnabled_scanImages` | `0` | Não faz scan binário a imagens — aceitável, baixo risco típico |
| `scansEnabled_checkHowGetIPs` | `1` | O próprio Wordfence **já tem um scan check dedicado a esta má-configuração** (ver §3) |
| `loginSec_maxFailures` / `_countFailMins` / `_lockoutMins` | `5` / `60` / `240` | Bloqueia ao fim de 5 falhas em 60 min, lockout de 4h |
| `loginSec_lockInvalidUsers` | `0` | Não bloqueia por tentar logins com username inexistente (só falhas de password em conta real) |
| `allowLegacy2FA` | `0` | ✅ não permite métodos 2FA fracos/antigos |
---
## 2. GAP real — 2FA não forçado, zero utilizadores protegidos
```bash
wp db query "SELECT name, val FROM ${PREFIX}wfconfig WHERE name='loginSec_requireAdminTwoFactor';" --allow-root --path=$PATH
# → 0 = 2FA NÃO é obrigatório para administradores
wp db query "SELECT COUNT(*) FROM ${PREFIX}wfls_2fa_secrets;" --allow-root --path=$PATH
# → 0 = zero utilizadores com 2FA activo, incluindo o admin
```
**Achado real (`emanuelalmeida.pt`):** `loginSec_requireAdminTwoFactor=0` e
**zero** linhas em `wfls_2fa_secrets` — o login continua protegido só por
password + rate-limit (5 tentativas/60min/lockout 4h), sem segundo factor,
mesmo sendo o site pessoal do fundador da empresa.
### Forçar 2FA para administradores
**Auditoria/leitura sempre segura.** Escrita é mutação em produção —
confirmar autorização antes de aplicar:
```bash
# Ler estado actual antes de alterar
wp db query "SELECT val FROM ${PREFIX}wfconfig WHERE name='loginSec_requireAdminTwoFactor';" --allow-root --path=$PATH
# Forçar 2FA para o role Administrator
wp db query "UPDATE ${PREFIX}wfconfig SET val='1' WHERE name='loginSec_requireAdminTwoFactor';" --allow-root --path=$PATH
```
**Cuidado operacional:** activar isto **sem** um administrador já ter um
autenticador configurado bloqueia o próprio acesso ao wp-admin no login
seguinte (o Wordfence força a configuração do 2FA no momento do login, não
tranca por fora — mas confirmar sempre com o utilizador antes, e nunca
aplicar em sessão headless sem acesso de recuperação garantido, ex. acesso
SSH/SQL directo para reverter o valor para `0` se algo correr mal).
---
## 3. GAP real — `howGetIPs` vazio atrás do Cloudflare
**Este é o achado mais concreto desta auditoria.** O próprio Wordfence já
tinha calculado a recomendação correcta e guardado numa chave separada, mas
**nunca a aplicou** à chave activa:
```bash
wp db query "SELECT name, val FROM ${PREFIX}wfconfig WHERE name IN
('howGetIPs','detectProxyRecommendation');" --allow-root --path=$PATH
```
```
name val
detectProxyRecommendation HTTP_CF_CONNECTING_IP
howGetIPs (vazio)
```
`detectProxyRecommendation` é preenchido automaticamente pelo Wordfence
quando detecta que o tráfego chega via um proxy conhecido (Cloudflare, neste
caso) — mas **fica só como sugestão**; `howGetIPs` (a chave que o WAF
realmente usa para decidir qual IP é o "real") continua vazia. Com
`howGetIPs` vazio, o Wordfence cai no fallback de ordem de headers
(`lib/wfUtils.php`: `HTTP_CF_CONNECTING_IP` → `HTTP_X_REAL_IP` →
`REMOTE_ADDR` → `HTTP_X_FORWARDED_FOR`), o que **na maioria dos casos até
funciona**, mas não é o comportamento explícito/auditável e não activa o
scan check dedicado (`scansEnabled_checkHowGetIPs=1` já está ligado — é
precisamente este scan que devia estar a assinalar isto como issue, mas
`wfpendingissues` está vazia, i.e. o scan periódico pode não ter corrido
recentemente ou o alerta expirou).
### Valores válidos de `howGetIPs` (confirmado no código, `wfConfig.php`/`wfUtils.php`)
Cabeçalhos HTTP reconhecidos pelo Wordfence para determinar o IP real:
| Valor | Quando usar |
|---|---|
| `HTTP_CF_CONNECTING_IP` | Site atrás de **Cloudflare** — este é o caso do bundle Descomplicar® (todos os sites usam App for Cloudflare®) |
| `HTTP_X_REAL_IP` | Proxy tipo nginx reverso simples (sem CDN) |
| `HTTP_X_FORWARDED_FOR` | Load balancer/proxy genérico |
| (vazio) | `REMOTE_ADDR` directo — só correcto se não houver proxy/CDN nenhum à frente |
Existe ainda `howGetIPs_trusted_proxy_preset`, que aponta para uma entrada
em `ipResolutionList` (JSON de ~8KB, sincronizado da API Wordfence, contém
blocos de IP de CDNs conhecidas — ex. `cloudflare`, `cloudfront` — usados
para validar que o pedido realmente vem de um proxy confiável antes de
aceitar o header). Consultar presets disponíveis:
```bash
wp db query "SELECT val FROM ${PREFIX}wfconfig WHERE name='ipResolutionList';" --allow-root --path=$PATH \
| python3 -c "import json,sys; print(list(json.load(sys.stdin).keys()))"
```
### Corrigir (copiar a própria recomendação do Wordfence)
```bash
# Ler antes de escrever
wp db query "SELECT val FROM ${PREFIX}wfconfig WHERE name='detectProxyRecommendation';" --allow-root --path=$PATH
# Aplicar — usa o valor que o próprio Wordfence já recomendou
wp db query "UPDATE ${PREFIX}wfconfig SET val='HTTP_CF_CONNECTING_IP' WHERE name='howGetIPs';" --allow-root --path=$PATH
# Confirmar
wp db query "SELECT name, val FROM ${PREFIX}wfconfig WHERE name='howGetIPs';" --allow-root --path=$PATH
```
Sem isto configurado, o rate-limiting de login, os bloqueios de IP do WAF e
o Live Traffic podem estar a agir sobre o IP do proxy Cloudflare em vez do
IP real do atacante — mais grave em sites com muito tráfego atrás do mesmo
edge, onde o "IP" observado pode ser partilhado por múltiplos visitantes.
---
## 4. Interacção com cache de página — `cacheType=disabled` é deliberado
```bash
wp db query "SELECT val FROM ${PREFIX}wfconfig WHERE name='cacheType';" --allow-root --path=$PATH
# → disabled
```
O Wordfence tem o seu próprio sistema de cache de página interno
(`Enable Falcon Engine`), mas em sites do bundle Descomplicar® **a cache de
página já é feita por outro plugin dedicado** (WP Fastest Cache ou
equivalente). `cacheType=disabled` está **correcto** — dois sistemas de
cache HTML activos ao mesmo tempo competem, servem versões desalinhadas e
dificultam o purge coordenado. **Nunca activar o cache Falcon do Wordfence
num site que já tem WP Fastest Cache/WP Meteor a fazer cache de página** —
confirmar sempre qual plugin de cache está activo antes de alterar
`cacheType` num site novo.
---
## 5. Scan settings — cobertura completa
```bash
wp db query "SELECT name, val FROM ${PREFIX}wfconfig WHERE name LIKE 'scansEnabled_%';" --allow-root --path=$PATH
```
| Categoria | Chave | Estado (piloto) |
|---|---|---|
| Core WordPress | `scansEnabled_core`, `_coreUnknown`, `_oldVersions` | `1` |
| Plugins/temas | `scansEnabled_plugins`, `_themes` | `1` |
| Malware/ficheiros suspeitos | `scansEnabled_malware`, `_fileContents`, `_fileContentsGSB`, `_suspectedFiles` | `1` |
| Espaço em disco | `scansEnabled_diskSpace` | `1` |
| Utilizadores admin suspeitos | `scansEnabled_suspiciousAdminUsers` | `1` |
| Opções de BD suspeitas | `scansEnabled_options`, `_suspiciousOptions` | `1` |
| Passwords fracas | `scansEnabled_passwds` | `1` |
| Comentários/posts (spam/malware injectado) | `scansEnabled_comments`, `_posts` | `1` |
| GeoIP / Google Safe Browsing | `scansEnabled_geoipSupport`, `_checkGSB` | `1` |
| Config legível externamente | `scansEnabled_checkReadableConfig` | `1` |
| **Detecção de IP mal configurada** | `scansEnabled_checkHowGetIPs` | `1` — ver §3 |
| Estado do WAF | `scansEnabled_wafStatus` | `1` |
| WPScan (directory listing, full path disclosure) | `scansEnabled_wpscan_directoryListingEnabled`, `_wpscan_fullPathDisclosure` | `1` |
| Sensibilidade alta | `scansEnabled_highSense` | `0` (desligado, reduz falsos positivos) |
| Scan binário de imagens | `scansEnabled_scanImages` | `0` (desligado) |
**Avaliação:** cobertura de scan quase total (23 de 25 checks activos),
única omissão relevante é a sensibilidade alta — aceitável para reduzir
ruído em sites com actividade legítima elevada.
---
## 6. Two-Factor / Passkeys / CAPTCHA — tabela SEPARADA `wfls_settings`
**Achado arquitectural importante desta expansão:** o módulo "Login
Security" (2FA, passkeys, CAPTCHA de login, XML-RPC, "remember device",
integração WooCommerce, NTP) **não guarda a sua configuração em
`wfconfig`** — vive numa tabela própria, `wfls_settings`, gerida pela
classe `WordfenceLS\Controller_Settings` (código em
`modules/login-security/classes/controller/settings.php`). Schema:
```
DESCRIBE ${PREFIX}wfls_settings;
name varchar(191) PRI
value longtext -- nota: coluna chama-se "value", não "val"
autoload enum('no','yes')
```
```bash
wp db query "SELECT name, value FROM ${PREFIX}wfls_settings ORDER BY name;" --allow-root --path=$PATH
```
As chaves `loginSec_requireAdminTwoFactor` e `loginSec_enableSeparateTwoFactor`
em `wfconfig` (§2) são a versão **legada** — um switch global único. A
versão actual do plugin é **baseada em papéis (roles)**:
`migrate_admin_2fa_requirements_to_roles()` migra o switch legado para
chaves por role (`required-2fa-role.<role>`) dentro de `wfls_settings` e
depois apaga a chave legada do `wfconfig`. Um site "antigo" pode ainda ter
o valor legado; um site já migrado tem-no ausente/zero e a fonte de
verdade passa a ser `wfls_settings` + capabilities.
### Chaves confirmadas em `wfls_settings` (`emanuelalmeida.pt`)
| Chave | Valor confirmado | O que controla |
|---|---|---|
| `xmlrpc-enabled` | `1` | Exigir 2FA também em pedidos XML-RPC autenticados |
| `allow-xml-rpc` | `1` | Permitir XML-RPC de todo (independente do 2FA) |
| `ip-source` | vazio (`automatic`) | Fonte do IP real para o módulo login-security — **independente** de `howGetIPs` do `wfconfig` (§3); valores: `automatic`, `REMOTE_ADDR`, `X-Forwarded-For`, `X-Real-IP` |
| `ip-trusted-proxies` | vazio | Lista de proxies confiáveis (newline-separated) para o cálculo de IP deste módulo |
| `2fa-user-grace-period` | `10` | Dias de período de graça depois de 2FA se tornar obrigatório por role, antes de bloquear o login |
| `require-2fa.administrator` | vazio | Legado (ver acima); use `required-2fa-role.administrator` |
| `passkey-sign-count-mode` | `reject-lower` | Política anti-clonagem de passkeys: `allow` / `reject-lower` / `reject-lower-and-zero` |
| `passkey-relying-party-override` | vazio | Override do hostname RP para passkeys (multi-domínio) |
| `remember-device` | vazio (`false`) | "Lembrar este dispositivo" para saltar 2FA X dias |
| `remember-device-duration` | `2592000` (30 dias) | Duração do "remember device" em segundos |
| `always-show-login-security-menu` | `1` | Mostrar sempre o menu Login Security mesmo sem nada configurado |
| `enable-auth-captcha` | vazio | CAPTCHA (reCAPTCHA) no formulário de login |
| `recaptcha-site-key` / `recaptcha-secret` | vazio | Credenciais reCAPTCHA — só relevantes se `enable-auth-captcha=1` |
| `recaptcha-threshold` | `0.5` | Limiar de pontuação do reCAPTCHA v3 (0–1) |
| `enable-woocommerce-integration` / `-account-integration` | vazio | Aplicar 2FA/passkeys também na área de conta WooCommerce |
| `use-ntp` | `1` | Sincronizar relógio via NTP para códigos TOTP (evita drift do relógio do servidor) |
| `whitelisted` | vazio | IPs isentos de exigência de 2FA (não confundir com `whitelisted` do `wfconfig`, que é do WAF) |
| `schema-version` | `3` | Versão do schema interno do módulo — não editar manualmente |
### Modelo de permissões por role (capabilities, não settings)
"2FA obrigatório" e "2FA opcional" por role **não** são uma chave de
configuração simples — são geridos como **capabilities** do WordPress,
sincronizadas por `WordfenceLS\Controller_Permissions`:
| Capability | Significado |
|---|---|
| `wf2fa_activate_2fa_self` | O utilizador pode activar/desactivar 2FA na própria conta |
| `wf2fa_activate_2fa_others` | Pode activar/desactivar 2FA de outros utilizadores |
| `wf2fa_manage_settings` | Pode editar as definições do módulo Login Security |
| `wfls_manage_passkey_self` / `_others` | Equivalente para passkeys |
| `wfls_show_login_security` | Capability interna que mostra/esconde o menu (sincronizada automaticamente) |
```bash
wp eval 'var_export(get_role("administrator")->capabilities);' --allow-root --path=$PATH | grep -i 'wf2fa\|wfls'
# Confirmado em emanuelalmeida.pt: administrator TEM wf2fa_activate_2fa_self (pode activar),
# mas isso é OPCIONAL — só se torna obrigatório se required-2fa-role.administrator tiver timestamp > -1
```
O estado "obrigatório desde quando" fica em `required-2fa-role.<role>`
(timestamp Unix) ou `-1` (não obrigatório) — chave dinâmica dentro de
`wfls_settings`, não aparece na lista de defaults porque só existe depois
de alguém a definir pela UI.
### Tabelas de dados do módulo (não config, mas relevantes para auditoria)
| Tabela | Conteúdo |
|---|---|
| `wfls_2fa_secrets` | Segredos TOTP activos por utilizador (contagem = utilizadores com 2FA activo) |
| `wfls_passkeys` | Passkeys (WebAuthn) registadas por utilizador |
| `wfls_role_counts` | Cache de contagens de utilizadores por role, usada pela UI |
---
## 7. Live Traffic — configuração completa
Todas as chaves vivem em `wfconfig` (ao contrário do módulo 2FA). Tabela
de dados: `wflivetraffichuman` (visitas humanas) + `wfhits`, `wfhoover`,
`wfcrawlers`, `wflocs`, `wflogins` (dados de suporte ao Live Traffic e ao
dashboard de tráfego).
| Chave | Valor (piloto) | O que controla |
|---|---|---|
| `liveTrafficEnabled` | `1` | Liga/desliga o registo de Live Traffic |
| `liveTraf_ignorePublishers` | `1` | Não regista visitas de utilizadores autenticados com capacidade de publicar (reduz ruído do próprio staff) |
| `liveTraf_ignoreUsers` | vazio | Lista de usernames (separados por vírgula) a ignorar |
| `liveTraf_ignoreIPs` | vazio | Lista de IPs (separados por vírgula) a ignorar |
| `liveTraf_ignoreUA` | vazio | User-agent a ignorar (substring) |
| `liveTraf_maxRows` | `2000` | Quantidade máxima de registos guardados |
| `liveTraf_maxAge` | `30` | Dias máximos de retenção dos registos |
| `liveTraf_displayExpandedRecords` | `0` | Preferência de UI (registos expandidos por omissão) — não afecta captura |
**Overrides que desligam Live Traffic independentemente da config:**
`wfConfig::liveTrafficEnabled()` força `false` se a constante
`WORDFENCE_DISABLE_LIVE_TRAFFIC` estiver definida, ou se
`WF_IS_WP_ENGINE` for verdadeiro (hosts WP Engine desactivam sempre este
módulo, independentemente do valor em `wfconfig`) — útil para explicar a
um cliente porque "liguei e não aparece nada" nesse tipo de hosting.
```bash
wp db query "SELECT name, val FROM ${PREFIX}wfconfig WHERE name LIKE 'liveTraf%' OR name='liveTrafficEnabled';" --allow-root --path=$PATH
```
---
## 8. Blocking completo — País, IP manual, Padrões (UA/Referrer/Hostname)
Todos os bloqueios (manuais e automáticos) vivem numa única tabela,
`wfBlocks7` (não em `wfconfig`), com um campo `type` que distingue a
natureza do bloqueio (`models/block/wfBlock.php`):
| Constante | Valor | Origem |
|---|---|---|
| `TYPE_IP_MANUAL` | 1 | Bloqueio de IP manual pelo admin |
| `TYPE_WFSN_TEMPORARY` | 2 | Resposta automática da Wordfence Security Network (rede partilhada entre sites) |
| `TYPE_COUNTRY` | 3 | Bloqueio por país |
| `TYPE_PATTERN` | 4 | Bloqueio avançado (IP range + hostname + user-agent + referrer) |
| `TYPE_RATE_BLOCK` | 5 | Gerado pelo rate limiting (§9), acção `block` |
| `TYPE_RATE_THROTTLE` | 6 | Gerado pelo rate limiting (§9), acção `throttle` |
| `TYPE_LOCKOUT` | 7 | Lockout de login (falhas de password/2FA) |
| `TYPE_IP_AUTOMATIC_TEMPORARY` / `_PERMANENT` | 8 / 9 | Bloqueios automáticos do WAF, temporário vs promovido a permanente por acção do admin |
```bash
# Ver todos os bloqueios activos por tipo
wp db query "SELECT type, COUNT(*) FROM ${PREFIX}wfblocks7 WHERE expiration=0 OR expiration>UNIX_TIMESTAMP() GROUP BY type;" --allow-root --path=$PATH
```
### Bloqueio por país
Campos guardados em `parameters` (JSON): `blockLogin` (bloquear só a
página de login), `blockSite` (bloquear o resto do site), `countries`
(array de códigos ISO). Comportamento configurado globalmente em
`wfconfig`:
| Chave | Valor (piloto) | O que controla |
|---|---|---|
| `cbl_action` | `block` | O que fazer a um país bloqueado: `block` (503) ou `redirect` (redireccionar) |
| `cbl_redirURL` | vazio | URL de destino se `cbl_action=redirect` |
| `cbl_loggedInBlocked` | `0` | Bloquear país mesmo que o visitante já tenha sessão iniciada |
| `cbl_bypassRedirURL` / `cbl_bypassRedirDest` | vazio | Um visitante de país bloqueado que aceda a este URL relativo é reencaminhado e recebe um cookie de bypass permanente |
| `cbl_bypassViewURL` | vazio | Um visitante permitido que veja este URL relativo recebe o cookie de bypass antecipadamente (para o caso de mudar de país/VPN depois) |
| `cbl_cookieVal` | gerado automaticamente | Valor único do cookie de bypass — regenerar invalida bypasses concedidos |
Nenhum destes está configurado no piloto (site sem bloqueio de país
activo) — confirmar sempre `hasCountryBlock()`/`countryBlocks()` antes de
assumir que não há impacto em visitantes internacionais.
### Bloqueio manual de IP / IP range
```bash
# Confirmar que um IP não está na whitelist antes de o bloquear (a UI faz isto automaticamente)
wp db query "SELECT val FROM ${PREFIX}wfconfig WHERE name='whitelisted';" --allow-root --path=$PATH
```
`wfBlock::isWhitelisted()` verifica dois níveis: a lista manual em
`whitelisted` (`wfconfig`, uma entrada por linha, IP/CIDR) e uma lista de
serviços conhecidos (`whitelistedServices`, JSON — vazio `{}` no piloto;
quando preenchido guarda ranges de IP de serviços como monitorização
externa que o Wordfence nunca bloqueia mesmo com regras activas).
### Bloqueio avançado (Custom Pattern) — hostname / user-agent / referrer
Bloqueio combinável por 4 critérios (pelo menos 1 obrigatório),
`type=custom-pattern` na validação (`wfBlock::validate()`):
| Campo | Formato | Exemplo de uso |
|---|---|---|
| `ipRange` | IP único, CIDR, ou range `1.2.3.4-1.2.3.9` (IPv4 e IPv6 não podem misturar-se no mesmo bloqueio) | Bloquear uma subnet inteira |
| `hostname` | wildcard `*` e `.` (regex `^[a-z0-9\.\*\-]+$`) | Bloquear por reverse-DNS de um datacenter conhecido |
| `userAgent` | substring livre | Bloquear scrapers com UA identificável |
| `referrer` | substring livre | Bloquear tráfego de referrer spam |
```bash
wp db query "SELECT id, blockedTime, reason, expiration FROM ${PREFIX}wfblocks7 WHERE type=4 AND (expiration=0 OR expiration>UNIX_TIMESTAMP());" --allow-root --path=$PATH
```
### Como o Wordfence decide não bloquear crawlers do Google
```bash
wp db query "SELECT val FROM ${PREFIX}wfconfig WHERE name='neverBlockBG';" --allow-root --path=$PATH
# Confirmado no piloto: neverBlockVerified
```
| Valor de `neverBlockBG` | Comportamento |
|---|---|
| `neverBlockVerified` (default, activo no piloto) | Verifica UA **e** faz PTR/reverse-DNS lookup para confirmar que é mesmo um IP do Google antes de isentar de bloqueio |
| `neverBlockUA` | Confia só no User-Agent, sem verificação de IP — menos seguro (fácil de fingir), mas mais barato |
| `treatAsOtherCrawlers` | Não dá tratamento especial a "Google" — pode ser bloqueado como qualquer outro crawler |
---
## 9. Rate Limiting completo — 5 tipos, `throttle` vs `block`
`models/block/wfRateLimit.php` define **5 limitadores independentes**,
cada um com um par de chaves `<nome>` (limiar) + `<nome>_action`
(`throttle` ou `block`). `DISABLED` como valor do limiar desactiva esse
limitador especificamente.
| Limitador | Chave limiar | Chave acção | Valor confirmado (piloto) | Aplica-se a |
|---|---|---|---|---|
| Global | `maxGlobalRequests` | `maxGlobalRequests_action` | `240` / `throttle` | **Qualquer** pedido, humano ou crawler — se activo, sobrepõe-se aos outros 4 |
| Page views de crawlers | `maxRequestsCrawlers` | `maxRequestsCrawlers_action` | `DISABLED` / `throttle` | Só User-Agents identificados como crawler (`wfCrawl::isCrawler`) |
| 404s de crawlers | `max404Crawlers` | `max404Crawlers_action` | `60` / `throttle` | Idem, mas só páginas 404 |
| Page views de humanos | `maxRequestsHumans` | `maxRequestsHumans_action` | `DISABLED` / `throttle` | Visitantes não identificados como crawler |
| 404s de humanos | `max404Humans` | `max404Humans_action` | `60` / `throttle` | Idem, mas só páginas 404 |
```bash
wp db query "SELECT name, val FROM ${PREFIX}wfconfig WHERE name LIKE 'max%Requests%' OR name LIKE 'max404%' OR name='blockedTime';" --allow-root --path=$PATH
```
**No piloto, o rate limit global (`maxGlobalRequests=240`) está
ACTIVO** — qualquer IP (humano ou bot) que exceda 240 pedidos na janela
de 1 minuto (tabela `wfTrafficRates`, campo `eMin`) é afectado. Os
limitadores dedicados a crawlers/humanos de page views estão desligados;
só os de 404 (60 num minuto) estão activos, o que é uma configuração
razoável (protege contra scans de 404 sem penalizar navegação normal).
**`throttle` vs `block` não são só rótulos — têm durações e severidade
diferentes:**
- `throttle` → `wfBlock::createRateThrottle()`, duração **fixa de 60
segundos** (`wfRateLimit::rateLimitThrottleDuration()`) — resposta 503
temporária, o visitante volta a conseguir aceder ao fim de 1 minuto.
- `block` → `wfBlock::createRateBlock()`, duração = `blockedTime`
(`wfconfig`, **300s / 5 min no piloto**, configurável) — bloqueio mais
longo, e pode ser promovido a permanente manualmente pelo admin na UI
de Blocking (`TYPE_RATE_BLOCK` → `TYPE_IP_AUTOMATIC_PERMANENT`).
**Excepção comum a 404s permitidos:** `allowed404s` (`wfconfig`, uma
entrada por linha, suporta wildcard `*`) — URLs que nunca contam para os
limitadores de 404, mesmo que devolvam 404 de facto. Default do piloto:
`/favicon.ico`, `/apple-touch-icon*.png`, `/*@2x.png`,
`/browserconfig.xml` (ícones/manifestos pedidos por browsers que geram
404 legítimos e não devem disparar rate limiting).
---
## 10. WAF — arquitectura de armazenamento, modos e categorias de regra
**Achado arquitectural crítico:** para o WAF funcionar *antes* do
WordPress carregar (Extended Protection / auto-prepend), o Wordfence
**não pode depender só da base de dados** — mantém uma cópia da config
relevante em ficheiros PHP no filesystem, fora de `wp_content/plugins`:
```bash
ls -la wp-content/wflogs/
# config.php — config nuclear do WAF (wafStatus, authKey, versão do formato) — 6 chaves só
# config-synced.php — MIRROR de chaves relevantes do wfconfig + bloqueios activos, sincronizado
# sempre que algo muda (país/IP/pattern blocks, howGetIPs, apiKey, etc.)
# config-livewaf.php — config activa/em uso pelo processo WAF autoprepended
# config-transient.php— cache de dados de ataque (grande, ~1.7 MB no piloto)
# rules.php — ficheiro PHP executável (não serializado) com as regras do WAF, incluído
# directamente no bootstrap; NÃO editar/apagar manualmente
# attack-data.php, ips.php — logs internos de ataques/IPs vistos pelo WAF
```
Estes ficheiros começam com `<?php exit('Access denied');
__halt_compiler(); ?>` seguido de dados serializados em PHP — só são
legíveis fazendo `substr()` a partir do offset de `__halt_compiler()` e
`unserialize()`. **Nunca editar estes ficheiros manualmente**; qualquer
alteração de config deve passar pela UI ou por `wfconfig`, que depois é
sincronizada automaticamente para estes ficheiros (via
`wfWAFIPBlocksController::setNeedsSynchronizeConfigSettings()`, chamado
sempre que `wfBlock::create*()` corre).
O motor de armazenamento é escolhido por `WFWAF_STORAGE_ENGINE`
(`waf/bootstrap.php`): **`file`** por omissão (confirmado no piloto —
ficheiros em `wflogs/` existem e têm conteúdo recente), ou **`mysqli`**
forçado automaticamente em hosts WP Engine/Flywheel (ou por constante
explícita), caso em que a config do WAF passa a viver numa ligação MySQL
directa (bypass ao `$wpdb`) em vez de ficheiros.
### Modos e níveis do WAF (`models/firewall/wfFirewall.php`)
| Constante | Valores possíveis | Significado |
|---|---|---|
| `firewallMode()` | `disabled` / `learning-mode` / `enabled` | Estado geral; **`enabled`** no piloto (§1, `waf_status`) |
| `protectionMode()` | `basic` / `extended` | `extended` requer `WFWAF_AUTO_PREPEND` activo (ficheiro `.user.ini`/`.htaccess` a fazer `auto_prepend_file` para `wordfence-waf.php` antes do WordPress arrancar) — sem isto, o WAF só corre depois do `muplugins_loaded`, mais tarde no ciclo, "Basic Protection" |
| `ruleMode()` | `community` / `premium` | Depende de `isPaid` — piloto usa feed `community` (regras com atraso de propagação face a sites Premium) |
| `blacklistMode()` | `disabled` / `enabled` | "Real-Time IP Blocklist" — **só disponível com `isPaid`**, mesmo que `disableWAFBlacklistBlocking=0` |
| `learningModeStatus()` | `false` / `true` / timestamp | Se em Learning Mode, indica se há mudança automática agendada para Enabled (`learningModeGracePeriodEnabled` + `learningModeGracePeriod`) |
### Categorias de regra (fail-score) — confirmado no `rules.php` do piloto
O motor de regras usa 3 categorias de ataque com pontuação de risco
própria (`$this->failScores[...]` em `wflogs/rules.php`):
```bash
ssh server "grep -o \"failScores\\['[a-z]*'\\]\" .../wp-content/wflogs/rules.php | sort -u"
# failScores['rce'] — Remote Code Execution
# failScores['sqli'] — SQL Injection
# failScores['xss'] — Cross-Site Scripting
```
Regras individuais podem ser desactivadas via `disabledRules` — essa
chave vive **dentro do storage do WAF** (`wp-content/wflogs/config.php`
no motor `file`), não em `wfconfig`; o piloto não tem nenhuma regra
desactivada (chave ausente = todas activas). A "pontuação de segurança"
mostrada no Dashboard (`wfFirewall::overallStatus()`) combina: % de
regras activas (35%), blacklist premium activa (35%), Extended Protection
(20%), Rate Limiting/Advanced Blocking ligado (10%) — pesado a favor do
WAF (80% do total), com brute-force protection a contar só 20%.
---
## 11. Diagnostics (Tools → Diagnostics)
Gerado por `lib/wfDiagnostic.php`, sem persistência em BD — é sempre
calculado on-demand quando a página carrega. Secções e o que cada uma
verifica:
| Secção | O que reporta |
|---|---|
| Wordfence Status | Versão instalada, versão da base GeoIP, jobs de cron em atraso (>30min) |
| Filesystem | Se o webserver consegue ler/escrever `wp-content/plugins/wordfence` e `wp-content/wflogs` |
| Wordfence Config | Teste de escrita/leitura real contra `wfconfig` (básico e serializado) — falha aqui = sem permissões de escrita na tabela |
| Wordfence Firewall | Auto-prepend activo, motor de storage configurado vs activo, path dos logs, permissões de ficheiros, conteúdo de `.htaccess`/`.user.ini` relacionado com o WAF |
| MySQL | Versão do MySQL + privilégios reais do utilizador da BD: `SELECT`/`INSERT`/`UPDATE`/`DELETE`/`CREATE`/`ALTER`/`DROP`/`TRUNCATE` (via `SHOW GRANTS FOR current_user()`) |
| PHP Environment | Versão PHP vs mínimo suportado, dono do processo PHP, suporte OpenSSL/cURL (versões, protocolos), `display_errors` |
| Connectivity | Ligação de saída a servidores Wordfence (https), ligação de volta ao próprio site (IPv4 e IPv6 — útil para detectar Cloudflare a bloquear o próprio site) |
| Time | Hora da rede Wordfence, hora do servidor, offset NTP, fonte de tempo usada para TOTP (2FA), fuso horário do WordPress |
**Casos de uso práticos:**
- `userCanAlter`/`userCanDrop` a `false` → migrações/upgrades do plugin
podem falhar silenciosamente; verificar antes de reportar "scan não
actualiza issues".
- `connectToSelf`/`connectToSelfIpv6` a falhar com sinais de Cloudflare
(`cf-mitigated: challenge`, `Cloudflare Ray ID` no body) → o próprio
Cloudflare está a desafiar o loopback request do Wordfence; relevante
para sites do bundle Descomplicar® (todos atrás de Cloudflare via
App for Cloudflare®) em diagnóstico de scans que nunca terminam.
- A secção "WordPress" (via `wfDiagnostic::getWordpressValues()`, não
listada acima por ser genérica ao core) expõe constantes como
`WP_DEBUG`, `DISALLOW_FILE_EDIT`, `WP_CACHE`, `FORCE_SSL_ADMIN` — útil
para confirmar hardening sem abrir `wp-config.php` directamente.
---
## 12. Tools → Import/Export de configuração
**Não é um ficheiro local** (ao contrário de outros plugins do bundle) —
`wfImportExportController` (`lib/wfImportExportController.php`) faz
`export()`/`import()` **através da API do wordfence.com**: o export
gera um token remoto (`export_options`), que depois se cola manualmente
no site de destino (`import_options` + token). Isto exige que ambos os
sites tenham `apiKey` válida e liguem para fora ao wordfence.com — não
funciona offline nem é um download/upload de ficheiro local.
O que é exportado: todas as `wfConfig::getExportableOptionsKeys()`
(essencialmente os grupos `checkboxes` + `otherParams` do
`$defaultConfig`, não o grupo `defaultsOnly`), o agendamento de scans
(`scanSched`, serializado), e todos os registos da tabela de bloqueios
(`wfBlock::exportBlocks()` — inclui bloqueios de país, IP e pattern, mas
não lockouts nem bloqueios automáticos temporários).
**Não é adequado para "clonar config para outro site" via SQL directo**
(copiar a tabela `wfconfig` entre sites não é suportado oficialmente e
arrasta chaves específicas da instalação como `encKey`/`longEncKey` —
preferir sempre o fluxo de token oficial acima ou reconfigurar via UI).
---
## 13. Wordfence Central — gestão remota multi-site
```bash
wp db query "SELECT name, val FROM ${PREFIX}wfconfig WHERE name LIKE 'wordfenceCentral%';" --allow-root --path=$PATH
```
**Achado confirmado no piloto: este site ESTÁ ligado ao Wordfence
Central** (`wordfenceCentralConnected=1`), associado a
`it@descomplicar.pt` desde 27/02/2025
(`wordfenceCentralConnectTime=1740679066`), com um `wordfenceCentralSiteID`
próprio (UUID). Central é a consola de gestão remota multi-site do
Wordfence (central.wordfence.com) — permite monitorizar/alertar e (em
planos pagos) configurar Wordfence em vários sites a partir de um único
painel, autenticado via JWT (`wordfenceCentralAccessToken`,
`wordfenceCentralUserSiteAccessToken`) trocado através de um par de
chaves pública/privada gerado no site (`wordfenceCentralPK`,
`wordfenceCentralSecretKey`).
| Chave relevante | O que é |
|---|---|
| `wordfenceCentralConnected` | `1`/`0` — liga/desliga a integração |
| `wordfenceCentralConnectEmail` / `_ConnectTime` | Quem ligou e quando |
| `wordfenceCentralPluginAlertingDisabled` | Se `1`, silencia alertas duplicados no próprio wp-admin porque já chegam via Central |
| `wordfenceCentralConfigurationIssue` | Flag interna — `1` indica falha na renovação do token JWT, útil para diagnosticar "Central parou de reportar" |
| `wordfenceCentralSiteData` | Cache JSON da última resposta da API Central sobre este site |
**Widget do dashboard nativo do WordPress:**
`email_summary_dashboard_widget_enabled` (`wfconfig`, `1` no piloto)
controla o widget "Activity Report" no `/wp-admin/index.php` (WordPress
Dashboard nativo) — **não confundir** com o próprio Dashboard do
Wordfence (`/wp-admin/admin.php?page=Wordfence`, componente Vue sem
chave de config individual para mostrar/esconder secções).
---
## Gotchas / erros comuns
| Sintoma | Causa | Solução |
|---|---|---|
| `wp option get wordfence_...` devolve vazio/erro | Config do Wordfence não vive em `wp_options` | Usar sempre `wp db query` contra `${PREFIX}wfconfig` (colunas `name`/`val`) |
| `SELECT value FROM wfconfig` falha (`Unknown column 'value'`) | A coluna chama-se `val`, não `value` | `SELECT name, val FROM ${PREFIX}wfconfig` |
| Contagem de 2FA por `usermeta.meta_key='wordfence_2fa_secret'` parece desactualizada | Essa é a chave legada; o módulo login-security moderno guarda em tabela própria | Preferir `SELECT COUNT(*) FROM ${PREFIX}wfls_2fa_secrets` |
| `howGetIPs` vazio mas o site parece "funcionar" na mesma | O Wordfence tem fallback de ordem de headers (`HTTP_CF_CONNECTING_IP` → `HTTP_X_REAL_IP` → `REMOTE_ADDR` → `HTTP_X_FORWARDED_FOR`) — mascara o problema em vez de o resolver | Configurar explicitamente `howGetIPs`, não confiar no fallback silencioso |
| `wfpendingissues` vazia mesmo com `scansEnabled_checkHowGetIPs=1` e `howGetIPs` mal configurado | O scan periódico pode não ter corrido recentemente, ou o alerta já expirou/foi dispensado | Não usar `wfpendingissues` vazia como prova de "nada errado" — cruzar sempre com leitura directa da config |
| Activar `loginSec_requireAdminTwoFactor` sem aviso prévio | Bloqueia o fluxo normal de login até o admin configurar um autenticador | Confirmar com o utilizador antes; garantir acesso alternativo (SSH/SQL) para reverter |
| Prefixo de tabela assumido como `wp_` | Sites CWP podem ter prefixo custom (confirmado `wpne_` no piloto) | `wp db prefix --allow-root --path=$PATH` sempre primeiro |
| Editar 2FA/passkeys/CAPTCHA via `UPDATE ${PREFIX}wfconfig` sem efeito | Essas definições vivem em `wfls_settings` (colunas `name`/`value`, não `name`/`val`) — ver §6 | `UPDATE ${PREFIX}wfls_settings SET value='...' WHERE name='...';` |
| Editar regras/bloqueios do WAF directamente em `wp-content/wflogs/*.php` | São ficheiros PHP serializados com `__halt_compiler()`, regenerados a partir da BD — editar à mão desalinha-os da BD e pode partir o WAF | Alterar sempre via `wfconfig`/UI; a sincronização para ficheiro é automática (§10) |
| Achar que `loginSec_requireAdminTwoFactor=0` prova que 2FA nunca é obrigatório | Essa chave é legada; sites migrados usam `required-2fa-role.<role>` dentro de `wfls_settings` | Verificar sempre `required-2fa-role.*` e as capabilities `wf2fa_*` do role antes de concluir (§6) |
| Assumir que `maxRequestsHumans`/`maxRequestsCrawlers`="DISABLED" significa "sem rate limiting nenhum" | Cada um dos 5 limitadores (§9) é independente — `maxGlobalRequests` pode estar activo e a aplicar-se a toda a gente mesmo com os outros 4 desligados | Verificar sempre os 5 pares `<limite>`/`<limite>_action`, não só um |
---
*Fonte: `CONFIG-Plugins-Referencia.md` §8 (Wordfence) + verificação
SQL/SSH ao vivo em `emanuelalmeida.pt` (16-08-2026, WP 7.0.4/PHP 8.2.31,
Wordfence 9.0.0 gratuito) — schema de `wfconfig` (313 chaves confirmadas
via `SELECT DISTINCT name`), contagem real de 2FA/passkeys,
`detectProxyRecommendation`, valores de scan e login security. **Expandido
nesta sessão** com leitura completa do código-fonte do plugin (466
ficheiros PHP, ~147k linhas) — `lib/wfConfig.php` (schema completo de
defaults, 3 grupos: `checkboxes`/`otherParams`/`defaultsOnly`),
`lib/wfJavascriptBridge.php` (labels humanos de todas as opções da UI),
`models/block/wfBlock.php` e `wfRateLimit.php` (tipos de bloqueio e os 5
limitadores de rate limiting), `models/firewall/wfFirewall.php` (modos e
scoring do WAF), `lib/wfDiagnostic.php` (secções de diagnóstico),
`lib/wfImportExportController.php` (mecanismo real de import/export via
token wordfence.com), `lib/wfCentralAPI.php` (integração Central) e o
módulo `modules/login-security/` completo — incluindo a descoberta de que
2FA/passkeys/CAPTCHA vivem numa tabela SQL separada (`wfls_settings`,
colunas `name`/`value`) e que o WAF mantém uma cópia de config em
ficheiros PHP serializados (`wp-content/wflogs/config*.php`), fora de
`wfconfig`. Confirmado ao vivo: `wfls_settings` completa, capabilities
`wf2fa_*`/`wfls_*` do role administrator, ficheiros WAF (`config.php`,
`config-synced.php`, `rules.php`), categorias de regra (`sqli`/`xss`/`rce`)
e ligação activa a Wordfence Central desde 27/02/2025.*