Guia de segurança¶
O que importa de verdade, em segurança, quando recebes pagamentos pela eupago com este SDK. Pouca teoria, muito foco nos erros específicos que custam dinheiro.
Dados de cartão: não os tens — que continue assim¶
Com a eupago, o form do cartão, o desafio 3-D Secure e as sheets do Apple Pay / Google Pay são todos hospedados pela eupago (ou pelas redes de cartões). O PAN nunca toca no teu servidor — e é isso que mantém a tua exposição PCI DSS em território SAQ A, a auto-avaliação mais leve que existe.
O corolário: nunca faças proxy nem reimplementes o form do cartão
para o checkout ficar "seamless". No momento em que números de cartão
passam pelo teu backend, herdas o fardo PCI completo. Redirecciona para
o payment_url, deixa a eupago trabalhar, espera pelo webhook.
A mesma lógica vale para a tua base de dados: não existe esquema em que guardar números de cartão seja a decisão certa. (A recipe Persistir pagamentos guarda ids, valores e estados — nada com forma de cartão.)
Conhece as tuas credenciais e o raio de explosão de cada uma¶
Tens até três segredos. Destrancam portas diferentes:
| Credencial | Destranca | Se vazar |
|---|---|---|
API key (api_key) |
Criar pagamentos num canal | O atacante cria pedidos de pagamento lixo em teu nome — chato, não directamente lucrativo |
Par OAuth (client_id / client_secret) |
A Management API inteira: reembolsos, listagem de transações, edição/revogação de subscrições — em todos os teus canais | O atacante reembolsa o teu dinheiro para wallets que controla. É a jóia da coroa |
| Chave de webhook ("Chave Criptográfica") | Verificar/decifrar webhooks (HMAC + AES-256-CBC) | O atacante forja webhooks que passam na verificação → eventos "Paid" falsos |
Regras práticas:
- Os três vivem em variáveis de ambiente ou num secrets manager — nunca no código, nunca na base de dados, nunca no repo. O SDK lê-os na construção do client; mais nada precisa deles.
- O par OAuth expira ao fim de 1 ano e pode ser revogado instantaneamente no backoffice (A Minha Conta → Credenciais). Põe a rotação no calendário; não descubras a expiração em produção.
- A chave de webhook roda-se no painel Webhooks 2.0 do canal. Se suspeitares de fuga, roda-a primeiro — é a mais barata de rodar.
- Sandbox e produção são mundos separados. Nunca deixes uma chave de
produção entrar num
.env.example, num log de CI ou no histórico de shell de um portátil.
O endpoint de webhook é a tua superfície de ataque¶
Qualquer um que descubra o URL do teu webhook pode fazer POST para lá.
O ataque clássico é exactamente tão básico quanto parece: enviar
{"status": "Paid"} e esperar que a loja envie a mercadoria. A defesa
é em camadas, e o SDK faz a parte difícil:
- Verificar antes de parsear. O
client.webhooks.parse()valida a assinatura HMAC (e decifra, nos canais encriptados) antes de devolver o que quer que seja. Um payload que falha lançaWebhookSignatureError— trata-o como hostil: loga (redigido), põe em quarentena, não mudes nada. - Idempotência. A eupago pode re-entregar. Faz hash do body cru e
torna a segunda entrega um no-op (ver o
on_webhook()da recipe). - Verificar o dinheiro. Assinatura válida ≠ negócio válido. Confirma que o valor e a moeda do webhook batem com o que guardaste ao criar o pagamento, e só depois fulfilla.
- HTTPS sempre, obviamente — o body do webhook tem PII de clientes.
Ver Verificação de assinatura para os detalhes ao nível do wire sobre o que é assinado e como.
O redirect não é um recibo¶
O success_url é para onde mandas o browser do cliente depois de um
fluxo hospedado. Não prova nada:
- O cliente pode guardá-lo nos bookmarks e revisitá-lo.
- Um atacante pode navegar directamente para lá sem pagar.
- O pagamento ainda pode falhar depois do redirect (edge cases 3DS).
Mostra lá algo simpático ("estamos a confirmar o teu pagamento…"), mas
os únicos eventos que viram uma encomenda para paga são um webhook
com assinatura verificada ou um poll autenticado à API da eupago. Nunca
faças fulfil_order() a partir de um GET no success_url.
PII: redige em todas as fronteiras¶
Os payloads de webhook e algumas respostas da API trazem telefones, emails e nomes. Há duas fronteiras a defender:
- Logs. O logger do próprio SDK (
eupago) redige automaticamente padrões de telefone, email e NIF — essa protecção vem de fábrica (e é por isso que não deves logar payloads da eupago pelo teu próprio logger "por conveniência"). - Armazenamento. Se persistes payloads brutos para auditoria, redige-os à entrada com o helper público:
from eupago.utils import redact_pii
guardado = redact_pii(webhook_payload)
# {"customer": {"email": "***EMAIL***", "phone": "***PHONE***"}, ...}
Combina redação com retenção: payloads brutos devem ter prazo de
validade (uma coluna purge_after, ou TTL no DynamoDB). Os factos
financeiros — valores, estados, ids de transação — não são PII e podem
viver para sempre; os payloads à volta deles não precisam. Esta
combinação (dados mínimos, payloads redigidos, retenção limitada) é a
maior parte da tua história RGPD para a tabela de pagamentos.
Torna o histórico à prova de adulteração¶
O teu event log de pagamentos é o que vais buscar numa disputa. Só é confiável se não puder ser editado em silêncio:
- O role de BD da aplicação recebe
INSERTmas nãoUPDATE/DELETEna tabela de eventos (ou o equivalente IAM no DynamoDB). - Pagamentos cancelados/falhados são um status, não uma linha apagada.
- Backups encriptados e genuinamente restauráveis — testa uma vez.
Identificadores vazam mais do que pensas¶
- Gera o
order_ida partir de um UUID, não de uma sequência. Ele aparece em URLs, webhooks e emails;ORD-000042conta ao mundo o teu volume de vendas e convida à enumeração dos teus endpoints. - Trata o
payment_urlcomo uma capability: quem tem o link vê o valor e (no Pay By Link) pode pagá-lo. Não o logues, não o deixes indexar.
Checklist¶
- [ ] Form de cartão / wallet sheets: hospedados pela eupago, nunca proxied
- [ ] Segredos em env/secrets manager; rotação OAuth no calendário (1 ano)
- [ ] Handler de webhook: verificar → quarentena em falha → dedup → check de valor → transição
- [ ]
success_urlmostra página "a confirmar…"; fulfilment só via webhook/poll - [ ] Payloads brutos guardados via
redact_pii()+ retenção limitada - [ ] Event log append-only por grant/policy
- [ ]
order_idnão-sequencial