Cerebro Studio · Backlog · Changelog
SeuImovel • /www/wwwroot/seuimovel.rio.br/docs/BACKLOG.md
Abrir Studio Projeto externo em modo read-only; encaminhamento permitido, escrita bloqueada.

Backlog Unificado

Projeto: SeuImovel. Fonte principal: /www/wwwroot/seuimovel.rio.br/docs/BACKLOG.md.

Modo read-only: ações de escrita ficam disponíveis apenas para o Cérebro.

Especificações Disponíveis (fora da fila pendente)

Detalhe do BK Selecionado

/www/wwwroot/seuimovel.rio.br/docs/backlog/BK-18-auditoria-seguranca-pagamento-pagseguro.md • 2026-07-25T12:58:22.825Z

BK-18 - Auditoria de segurança focada em pagamento (PagSeguro) - Julho/2026

Objetivo

Registrar os achados da auditoria de segurança pedida pelo dono do site em 24/07/2026, com foco no fluxo de pagamento (compra de créditos via PagSeguro) e na configuração de produção. Correção de premissa: o site não usa Mercado Pago — usa PagSeguro (imoveis/views.py, webhook_pagseguro, PAGSEGURO_EMAIL/PAGSEGURO_TOKEN). Não há nenhuma referência a Mercado Pago no projeto.

Todos os achados abaixo foram confirmados ao vivo (requisição HTTP real ao site em produção ou leitura direta do código/config).

Status

  • [x] Item 1 (CRÍTICO — DEBUG=True em produção) — corrigido e validado em 24/07/2026
  • [ ] Item 1b (CRÍTICO — SECRET_KEY de produção commitada no Git) — fora de escopo por decisão do dono, aguardando janela de baixo tráfego para rotacionar
  • [x] Item 2 (ALTO — PAGSEGURO_SANDBOX acoplado ao DEBUG) — corrigido e validado em 25/07/2026
  • [x] Item 3 (MÉDIO — .env com permissão 644) — corrigido e validado em 25/07/2026
  • [x] Item 4 (MÉDIO — deduplicação do webhook por texto livre) — corrigido e validado em 25/07/2026
  • [x] Item 5 (MÉDIO — db.sqlite3 parado dentro do webroot) — corrigido e validado em 25/07/2026
  • [x] Item 6 (MÉDIO — webhook PagSeguro sem rate limit dedicado) — corrigido e validado em 25/07/2026
  • [x] Item 7 (achado extra, mesma categoria do BK-341 item 5 — TLS 1.1 habilitado) — corrigido e validado em 25/07/2026

1. CRÍTICO — DEBUG=True ativo em produção

Confirmado ao vivo: uma URL inexistente (https://seuimovel.rio.br/uma-rota-que-nao-existe-teste-999/) retornava a página técnica de erro do Django (lista completa de URL patterns, stack, etc.) em vez de um 404 genérico. Isso acontece porque o .env de produção não define DEBUG, e seuimovelrio/settings.py tem DEBUG = config('DEBUG', default=True, cast=bool) — cai no default inseguro.

Isso contraria a própria documentação do projeto: DEPLOY_NOTAS.md já diz explicitamente "DEBUG: Deve estar False no .env" em produção — ou seja, é uma regressão/lacuna de configuração, não uma decisão consciente.

Correção aplicada: adicionado DEBUG=False ao .env de produção e reiniciado o serviço Gunicorn (seuimovelrio.service, confirmado como o único serviço ativo — gunicorn-seuimovel.service está failed desde jan/2026 e não deve ser usado).

Backup do arquivo mexido: /www/wwwroot/seuimovel.rio.br/.env.bkp_original_20260724-1749_BK-18-debug-false

Se o site cair depois desta mudança, restaurar esse .env e rodar systemctl restart seuimovelrio.service.

Validado em 24/07/2026:

  • curl https://seuimovel.rio.br/uma-rota-que-nao-existe-teste-999/ → agora mostra 404 genérico ("Not Found"), sem stack técnico
  • Home (/) → 200
  • /imoveis/ (listagem) → 200
  • systemctl status seuimovelrio.serviceactive (running) após o restart, sem erro nos logs iniciais

1b. CRÍTICO (achado durante a correção do item 1) — SECRET_KEY de produção commitada no histórico do Git

Ao abrir o .env pra fazer o backup antes do fix do item 1, percebi que o SECRET_KEY usado em produção é exatamente o mesmo valor que está hardcoded como default no seuimovelrio/settings.py:

django-insecure-fo%tx0o*7r1pn4)nv-=#-r!dd5gh_4o+1_!gn04^m2lo0rbulp

Confirmado que esse literal aparece 5 vezes no histórico do Git (git log -p --all -- seuimovelrio/settings.py), num repositório com remote github-rionoteatro:GeJoRei/seuimovelrio.git. Ou seja: quem tiver acesso de leitura a esse repositório (colaborador, token vazado, ou o repo virar público em algum momento) já tem a chave real de produção — que assina sessões Django, tokens de CSRF e tokens de redefinição de senha. Na prática, quem tem essa chave consegue forjar sessão de qualquer usuário (inclusive admin) e forjar link de redefinição de senha sem precisar do e-mail da vítima.

Por que não corrigi junto com o item 1: rotacionar o SECRET_KEY desloga todo mundo que está logado no momento e invalida qualquer link de redefinição de senha pendente. Isso não derruba o site, mas é um impacto operacional/de usuário que prefiro fazer com ciência explícita do dono, não como efeito colateral de outro fix.

Correção proposta (aguardando decisão):

  1. Gerar uma chave nova e aleatória (python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())").
  2. Colocar essa chave só no .env de produção (nunca no settings.py).
  3. Trocar o default hardcoded do settings.py para algo que falhe alto se .env não tiver SECRET_KEY, em vez de usar um valor real como fallback (ex.: manter um default óbviamente de desenvolvimento, tipo "dev-only-insecure-key", e nunca reaproveitar esse valor em produção).
  4. Avisar que todos os usuários logados serão deslogados no momento da troca — bom fazer em horário de baixo tráfego.
  5. (Opcional, mas recomendado) verificar/alterar a visibilidade do repositório GeJoRei/seuimovelrio no GitHub e revisar quem tem acesso.

2. ALTO — PAGSEGURO_SANDBOX acoplado ao mesmo valor de DEBUG

settings.py: PAGSEGURO_SANDBOX = config('DEBUG', default=True, cast=bool). Como DEBUG estava caindo em True (item 1), o gateway de pagamento pode ter rodado em modo sandbox em produção por um período — precisa ser confirmado olhando o histórico de transações/logs do PagSeguro, já que isso pode ter mascarado ou distorcido cobranças reais.

Ação: conferir com o PagSeguro (painel/API) se houve compra de crédito real que caiu no ambiente sandbox por engano — isso ainda não foi conferido, é ação manual pendente do dono, eu não tenho acesso ao painel do PagSeguro.

Correção aplicada em 25/07/2026: PAGSEGURO_SANDBOX agora lê variável própria (config('PAGSEGURO_SANDBOX', default=False, cast=bool) no settings.py, PAGSEGURO_SANDBOX=False explícito no .env), desacoplada do DEBUG.

Backups: seuimovelrio/settings.py.bkp_original_20260725-0950_BK-18-item2-sandbox e .env.bkp_original_20260725-0950_BK-18-item2-sandbox.

Validado em 25/07/2026: systemctl restart seuimovelrio.service sem erro, home e /imoveis/ continuam 200. Não fiz uma compra real para confirmar o ambiente PagSeguro (ver item 4, zero transações COMPRA existem hoje em produção) — só confirmei que o app sobe normalmente lendo a nova variável.


3. MÉDIO — .env com permissão 644 (legível por qualquer usuário local)

No rionoteatro.com.br o .env está em 640 (mais restritivo). No seuimovel.rio.br estava em 644 — qualquer conta no servidor conseguia ler SECRET_KEY, DATABASE_URL e PAGSEGURO_TOKEN.

Correção aplicada em 25/07/2026: chown www:www + chmod 640 no .env (confirmado que seuimovelrio.service roda como User=www Group=www, então o processo continua lendo normalmente).

Validado em 25/07/2026: ls -la-rw-r----- www www; restart do serviço sem erro de permissão nos logs; home//imoveis/ 200.


4. MÉDIO — Deduplicação do webhook do PagSeguro por texto livre

imoveis/views.py::webhook_pagseguro usa TransacaoCredito.objects.filter(descricao__icontains=transaction.code).exists() para não creditar duas vezes a mesma compra — comparação de substring em campo de descrição livre, não um campo indexado/único de "código de transação processado". Funciona na prática, mas é frágil (falso positivo se um código for substring de outro).

O webhook em si segue o padrão correto de segurança: não confia no POST recebido, sempre revalida via pg.check_notification(notification_code) direto na API do PagSeguro antes de creditar.

Correção aplicada em 25/07/2026: campo codigo_transacao = models.CharField(max_length=64, unique=True, null=True, blank=True) em TransacaoCredito (migration 0008_transacaocredito_codigo_transacao, aditiva — sem apagar/alterar dado existente). webhook_pagseguro em views.py passou a checar TransacaoCredito.objects.filter(codigo_transacao=transaction.code).exists() em vez do descricao__icontains.

Antes de migrar, tirei um backup lógico completo do banco via python manage.py dumpdata (/root/.security_followup/seuimovel_db_backups/dumpdata_20260725-0955_pre_BK18_item4_migration.json, 92KB). Também confirmei que hoje existem 0 transações do tipo COMPRA em produção (as 21 transações existentes são ADMIN/USO_ANUNCIO/CUPOM) — ou seja, não havia nada real para conciliar/backfillar nesse campo, o que tornou essa migration de risco bem baixo.

Backups: imoveis/models.py.bkp_original_20260725-0950_BK-18-item4-codigo-transacao, imoveis/views.py.bkp_original_20260725-0950_BK-18-item4-codigo-transacao.

Validado em 25/07/2026: migration aplicada sem erro; TransacaoCredito.objects.count() continua 21 depois da migration (nada perdido); as 21 linhas antigas ficaram com codigo_transacao=NULL (permitido, Postgres aceita múltiplos NULL numa coluna unique); serviço reiniciado sem erro; home//imoveis/ 200.


5. MÉDIO — db.sqlite3 parado dentro do webroot

Existe um db.sqlite3 na raiz do projeto (/www/wwwroot/seuimovel.rio.br/db.sqlite3), mas a produção real usa PostgreSQL via DATABASE_URL (conforme DEPLOY_NOTAS.md: "Banco: PostgreSQL 12.22"). Ou seja, provavelmente é um artefato de desenvolvimento esquecido, não o banco real — mas por estar dentro do document root, depende só do roteamento do Django/Nginx não expor arquivos estáticos ali (hoje não expõe, confirmado com curl → 404).

Correção aplicada em 25/07/2026: confirmado que é lixo de dev — db.sqlite3 tinha data de modificação de 02/11/2025, só 1 usuário e 0 imóveis (produção real, via Postgres, tem muito mais que isso). Movido (não apagado) para fora do webroot: /root/.security_followup/seuimovel_dev_leftovers/db.sqlite3.bkp_original_20260725-0952_BK-18-item5-removido-do-webroot.

Validado em 25/07/2026: curl https://seuimovel.rio.br/db.sqlite3 → 404 (já era 404 antes, mas agora o arquivo nem existe mais fisicamente no document root); home//imoveis/ 200; app não usa esse arquivo (confirmado via DATABASE_URL=postgres://... no .env, sem fallback ativo pro SQLite).


6. MÉDIO — Webhook do PagSeguro sem rate limiting dedicado

Cada chamada em /pagseguro-webhook/ faz uma requisição de rede síncrona à API do PagSeguro. Sem limite de taxa, um flood pode causar esgotamento de workers do Gunicorn ou rate-limit da conta no PagSeguro.

Correção aplicada em 25/07/2026: nova zona limit_req_zone $binary_remote_addr zone=pagseguro_webhook_rl:10m rate=120r/m; em nginx.conf e location = /pagseguro-webhook/ { limit_req zone=pagseguro_webhook_rl burst=20 nodelay; ... proxy_pass http://127.0.0.1:8000; } no vhost, antes do location / genérico que faz o proxy pra Gunicorn.

Backups: seuimovel.rio.br.conf.bkp_original_20260725-0951_BK-18-item6-webhook-rl-tls e nginx.conf.bkp_original_20260725-0951_BK-18-item6-zone.

Validado em 25/07/2026: 22-23ª requisição consecutiva a /pagseguro-webhook/ já retorna 429; home//imoveis/ continuam 200.


7. Achado extra (fora da lista original, encontrado durante o item 6) — TLS 1.1 habilitado

Igual ao BK-341 item 5 do rionoteatro.com.br: o vhost do seuimovel.rio.br também tinha ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;. Mesmo risco (abaixo do mínimo PCI-DSS, site processa pagamento via PagSeguro).

Correção aplicada em 25/07/2026: ssl_protocols TLSv1.2 TLSv1.3; (removido TLSv1.1). Mesmo backup do item 6.

Validado em 25/07/2026: conexão forçando --tls-max 1.1 falha; --tlsv1.2 continua 200.


Achado extra não corrigido (fora de escopo, só registrado) — WA_API_KEY com default hardcoded no código

Em settings.py: WA_API_KEY = config('WA_API_KEY', default='rnt_secret_key_2026_secure_v1') — mesmo padrão de risco do item 1b (SECRET_KEY): um valor de segredo "real-parecendo" como fallback hardcoded no código-fonte, versionado no Git. Não faz parte do escopo desta auditoria (que é sobre pagamento) e não foi tocado nesta execução — registrando aqui só para não perder o achado. Vale revisar se esse valor é o mesmo usado de fato em produção (igual ao que aconteceu com o SECRET_KEY do item 1b) numa auditoria futura.


Validação

Ver seção "Validado em 24/07/2026" dentro do item 1 acima (item 1). Itens 2 a 7: ver "Validado em 25/07/2026" dentro de cada item.