-
Status: complete
-
Intake: req-163.md
-
Data: 2026-09-15
-
Classificação: implementação de batch (segurança / governança do núcleo)
-
Modo de autonomia: supervisionado (sem commit, push ou deploy)
Objetivo
-
Uploads com progresso via
XMLHttpRequestcru deixam de voltar 403 com o usuário logado. -
Instalações de desenvolvimento, homologação ou privadas podem ser trancadas por
.env/tela, com restrição por perfil enoindex, nofollowobrigatório.
Pilar 1 — XMLHttpRequest em gestor/assets/global/global.js
-
Envelope de
XMLHttpRequest.prototype.open(memoriza método/URL e zera o registro de cabeçalhos),setRequestHeader(registra o nome em minúsculas) esend(injetaX-CSRF-TokenemPOST/PUT/PATCH/DELETEde mesma origem quando ninguém o definiu). Guarda__c2fCsrf. -
$.ajaxtambém passa por este envelope (o jqXHR delega ao XHR nativo). O prefilter do jQuery chamasetRequestHeadere o registro impede duplicar o token — o caso "token manual" do teste cobre exatamente esse caminho. -
401 com
X-Gestor-Auth-Redirectno eventoloadreaproveitaredirecionarParaLogin(), que já tem trava contra redirecionamento duplo (oajaxErrordo jQuery dispara no mesmo XHR). -
Falha interna do envelope nunca impede o envio (mesma política do envelope de formulário).
-
global.min.jsregenerado porassets:minify(24,9 KB → 10,3 KB); a guarda__c2fCsrfe os envelopes do prototype estão presentes no derivado.
Pilar 2 — Acesso Restrito ao Site
Núcleo
-
config.php:site-restricted-access(bool) esite-restricted-profilesa partir do.env. -
bibliotecas/gestor.php— quatro funções, três delas PURAS:gestor_site_acesso_restrito_ativo()($_CONFIG→$_ENV/getenv),gestor_site_acesso_restrito_perfis()(só inteiros positivos, sem repetição),gestor_site_acesso_restrito_rota_isenta()egestor_site_acesso_restrito_perfil_autorizado(). -
gestor.php:-
gestor_roteador_acesso_restrito()chamado emgestor_roteador()antes da consulta da página:sem_permissaoe.ajax.publicnão abrem brecha, e o visitante sem login não distingue página existente de inexistente. -
Sem sessão →
redirecionar-localna sessão + 401 →signin/(AJAX: JSONAUTH_REQUIRED+X-Gestor-Auth-Redirect, o mesmo contrato que oglobal.jsjá consome). -
Perfil não autorizado →
gestor_roteador_acesso_restrito_negado(): 403 em tela própria com botãosignout/(AJAX: JSON 403). Textos em variáveis globaissite-restricted-denied-*(pt-br/en). -
X-Robots-Tag: noindex, nofollowemitido pelo porteiro em TODA resposta do site restrito (inclusive redirecionamento e recusa) e<meta name="robots">forçada emgestor_pagina_extra_head_e_javascript()— uma única tag quando a rota também é de sistema.
-
Tela admin-environment
-
Seção "Acesso Restrito ao Site" na aba "Configurações do Site", antes de "HTML entregue ao navegador" (pt-br). No
enela entra antes de "Bots and Crawlers", que é a segunda seção daquele template — oennão tem a seção de limpeza de HTML (drift pré-existente do req-132, não tratado aqui). -
Toggle, busca com debounce de 250 ms (
ajaxOpcao: 'buscar-perfis', perfis ativos do idioma, máx. 10, busca por nome, slug ou id), badges com "x" e input oculto como lista autoritativa. Respostas fora de ordem são descartadas por número de sequência. -
Persistência de
SITE_RESTRICTED_ACCESS(sótrue/false) eSITE_RESTRICTED_PROFILES(normalizado pela mesma função do núcleo) no.envviaadmin_environment_env_write(). -
Módulo
1.0.1→1.0.2; páginasadmin-environmenten1.24→1.25, pt-br1.26→1.27.
Decisões tomadas na implementação
-
O perfil vem de
gestor_usuario(), não degestor_usuario_perfil(). O intake citava a segunda, mas ela devolve o cookieauthprofile, que não é assinado: o próprio visitante poderia escolher o perfil com que entra.gestor_usuario()resolve o registro do banco a partir do JWT já validado porgestor_permissao_token(). Um teste de contrato guarda a ausência da chamada insegura. -
Perfil recusado recebe 403, não redirecionamento. Mandar um usuário já logado ao
signin/faria o login devolvê-lo ao painel, que o recusaria de novo — laço. A saída oferecida ésignout/. -
Trava contra autobloqueio no salvamento. Ligar a restrição com lista que não inclui o perfil de quem salva tranca o operador para fora da própria tela, e a única saída passaria a ser editar o
.envno servidor. O salvamento é recusado comsite-restricted-lockout; lista vazia continua permitida (qualquer logado entra). -
Rotas isentas pelos caminhos reais. O intake listava nomes em português (
/esqueci-minha-senha/,/redefinir-senha/…); as rotas doperfil-usuariosãoforgot-password/,redefine-password/,signin-2fa/,oauth-callback/,email-confirmation/etc. As duas grafias estão na lista. Rotas de sistema (cookies-is-mandatory/, páginas de erro) entram viagestor_pagina_rota_sistema(). -
_api/,_gateways/e estáticos já saem emgestor_config(), antes do roteador. A lista de isenção os repete como defesa em profundidade, não como mecanismo principal.
Pilar 3 — Governança
-
Skill
c2f-javascript-ajaxganhou a tabela de cobertura automática de CSRF/401 doglobal.js, incluindoXMLHttpRequeste a orientação para uploads com progresso. Replicada nos 5 espelhos do repositório (.codex,.claude,.cursor,.github,.gemini), MD5 idêntico. -
Pendente: os espelhos em
conn2flow-ai-workspace(raiz e kits detemplates/) são outro repositório e não foram tocados.
Validação executada
| Verificação | Resultado |
|---|---|
php -l (gestor.php, bibliotecas/gestor.php, config.php, admin-environment.php) |
4/4 sem erro |
node --check (global.js, admin-environment.js) |
OK |
git diff --check nos arquivos de autoria |
OK |
Vitest focado global-csrf.test.js |
17/17 (6 novos: POST, GET, cross-origin, token manual, guarda, 401) |
PHPUnit focado SiteAcessoRestritoReq163Test |
16/16 (62 asserções) |
php cli/c2f.php assets:minify |
2 gerados, 63 coerentes, 0 falhas |
php cli/c2f.php resources:sync --force |
2.856 recursos, 237/237 Tailwind, "Nenhum problema detectado" |
| Vitest completo | 423/423 |
PHPUnit completo (--order-by=default) |
1.174/1.174 (7.792 asserções, 4 skipped pré-existentes) |
Ressalvas de ambiente
-
PHP do host (8.5.8 via WinGet) sem
pdo_sqlitehabilitado e semOPENSSL_CONF: a primeira rodada deu 16 erros (15could not find driver+ 1 geração de chave RSA). Sem alterar ophp.inido operador, a suíte foi rodada comPHP_INI_SCAN_DIRapontando para um ini temporário (extension=pdo_sqlite,extension=sqlite3) eOPENSSL_CONF=<php>\extras\ssl\openssl.cnf. -
resources:sync: a etapa final de publicação emdist/não roda semPUBLIC_PATH; os recursos foram sincronizados e as URLs seguem resolvendo peloarquivo-estatico. -
./c2f assets:minifypelo wrapper Git Bash falha (oexecdonpxnão resolve nesse ambiente); pelo PHP nativo (php cli/c2f.php assets:minify) funciona.
Achado fora de escopo — acoplamento entre testes pré-existentes
Com a ordem do phpunit.xml (executionOrder="depends,defects", que usa o cache de resultados e
durações), a suíte alterna entre verde e 2 falhas em ForcarAtualizacaoTest. Causa medida pelo
log de eventos: quando ProjectIdentityPassthroughTest roda antes, o static $meta de
schemaMetadata() (atualizacoes-banco-de-dados.php:277) fica congelado com o diretório temporário
dele. Reproduzido idêntico sem o teste deste lote (--exclude-filter: verde/falha/verde), portanto
não é regressão do req-163. No CI o cache nasce vazio e a ordem padrão passa. Correção sugerida para
intake próprio: dar a schemaMetadata() um meio de reset para testes, ou isolar as duas classes em
processo separado.
Pendências de homologação (operador)
-
Deploy
Update => Core(página, variáveis e JS vêm do banco/derivados). -
Com
SITE_RESTRICTED_ACCESS=true: página pública sem login →/signin/;/signin/sem laço; CSS/imagens 200;_api/e_gateways/sem cookie; perfil fora da lista → 403;falsevolta ao acesso público;noindex, nofollowna meta e no cabeçalho. -
Upload real com progresso via XHR em módulo logado sem 403.
-
Não houve validação em navegador real nesta rodada.