--- 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.`) 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.` (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 `` (limiar) + `_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 `` 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.` 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 ``/`_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.*