medicus.global · 2026-05-05
"Todas as informações fui eu que as introduzi, e consinto que o
pessoal de socorro e os médicos as possam ler — para lhes dar o
melhor conhecimento prévio possível da minha situação pessoal,
especialmente numa emergência. O meu amor pela vida é maior do
que as regras do país sobre proteção de dados."
Essa é a decisão adulta e voluntária por trás de cada perfil My Medicus criado. A nossa tarefa é honrar a escolha da paciente: guardar os dados em segurança, mostrá-los apenas a quem tem o PIN correto, deixar a paciente ver cada leitura, e permitir que a paciente revogue o cartão no segundo em que é perdido. O resto deste documento explica como.
"Qual é a vossa segurança em relação ao My Medicus?"
Numa emergência, o maior desejo da paciente é recuperar. É a quem servimos. A segurança é o meio, não o fim.
Três princípios estão acima de tudo:
Documentámos 19 cenários de ataque diferentes contra o My Medicus e executámos cada um contra o sistema de produção. O resultado está descrito abaixo — sem linguagem de marketing, sem "padrão da indústria", sem "best practice". Apenas o que foi concretamente testado.
19 cenários de ataque contra duas contas na base de dados em produção — uma conta demo e um novo utilizador real. Ferramentas: pedidos HTTP diretos, execução paralela, introspeção SQL da base de dados de produção, parsing de respostas.
Resultado: 14 ataques encontraram defesas que aguentaram. 5 vulnerabilidades foram encontradas e fechadas no mesmo dia.
| Ataque | Por que falhou o ataque |
|---|---|
| Roubo de toda a base de dados | Alergias, medicação, identificadores nacionais, passaporte, CESD e números de seguro estão cifrados com AES-256-GCM. A chave vive numa loja de segredos separada, nunca na base de dados. Se roubares o ficheiro DB, obténs texto ilegível |
| Força bruta sobre PIN de 6 dígitos | Atrasos progressivos ativam-se à 5ª tentativa errada: 5 seg → 30 seg → 60 seg entre cada |
| Injeção SQL no formulário de login | Todas as consultas à base de dados usam consultas parametrizadas — cinco payloads de injeção diferentes foram rejeitados |
| Chamadas API diretas sem login | Todos os endpoints protegidos devolvem HTTP 401 |
| Adivinhar tokens | Os tokens são 32 caracteres hex derivados via SHA-256. Espaço de procura: 16^32. 50 tentativas aleatórias → 0 acertos |
| Path-traversal a backups ou config | /_db_backups/, /.env, /.git, /medicus.db, /wp-config.php devolvem todos 404 |
| Enumeração de emails via "esqueci-me da senha" | Resposta genérica consistente independentemente de o email existir — sem fuga de informação |
| XSS em campos públicos | Servidor rejeita entrada malformada, sem reflexão de HTML/JS |
| Roubo físico de carteira sem PIN do cartão | Bloqueio PIN para todas as tentativas incorretas |
| 10 tentativas paralelas de força bruta | Rate-limit aplica-se por token, não por pedido |
| Eliminação do registo de auditoria via endpoint DELETE | Nenhum endpoint DELETE exposto |
Adivinhar admin-token (admin, 1234, letmein) |
Todos devolvem 401 |
| Tentar fazer o servidor vazar segredos via payloads de erro | Nem chave de cifragem nem segredos Stripe aparecem na saída de erro |
| Abuso cross-origin de outro site | Browser bloqueia por same-origin policy |
| # | O quê | Severidade | O que foi feito |
|---|---|---|---|
| F1 | Login respondia 200 ms mais lentamente quando o email estava registado do que quando não estava. Um atacante podia determinar se uma conta existe | 🔴 Crítico | Login agora executa verificação Argon2id sempre (também contra emails desconhecidos, com hash dummy). Tempo de resposta constante. Mensagem de erro genérica |
| F2 | Sem cabeçalhos de segurança (HSTS, CSP, X-Frame, X-Content-Type, Referrer-Policy, Permissions-Policy) | 🔴 Crítico | Todos os 6 cabeçalhos são agora adicionados em todas as respostas via middleware. CSP bloqueia script/img/style/frame em sources em whitelist |
| F3 | Um cartão roubado tem QR + PIN impressos diretamente. O ladrão tem acesso até o dono revogar | 🟠 Alto | (1) Paciente vê cada leitura no seu painel ("Meu registo" — prefixo IP, país, hora). (2) Dono pode revogar o cartão com um clique → todos os cartões impressos morrem instantaneamente + novo PIN gerado |
| F4 | /admin devolvia SPA-HTML a todos, revelando a existência do painel admin |
🟡 Baixo | Devolve agora o mesmo 404 que rotas inexistentes para utilizadores não autenticados |
| F5 | Esqueci-me da senha podia ser abusado para inundar a caixa de uma vítima (1.000 mails/hora) — e simultaneamente prejudicar a nossa reputação de remetente | 🟠 Alto | Se o utilizador tem um token reset válido (máx 2 horas), é reutilizado em vez de enviar novo mail. Mais rate-limit por email de 2/min |
| Escolha | Justificação |
|---|---|
| Sem autenticação nacional na vista paramédico | A paciente está inconsciente no campo de golfe. O paramédico não tem autenticação nacional, e mesmo que tivesse, não temos 90 segundos para os autenticar. PIN-no-cartão é a resposta prática |
| Não "zero-knowledge" | O servidor PODE decifrar os campos — é condição para que o paramédico possa ver os dados. Não chamamos "zero-knowledge" ao que não é |
| Sem importação automática de dados | Cada campo é escolhido pelo utilizador. Sem auto-import de outras plataformas de saúde |
Se um paramédico ler legitimamente o cartão com o PIN correto, pode fotografar o ecrã. Não podemos impedi-lo — é o preço de os dados sequer poderem ser mostrados numa emergência. Defesa:
É o mesmo modelo de segurança que uma pulseira de identificação médica física para diabéticos: os dados são visíveis ao pessoal de socorro, aceitamos que um carteirista também os possa ver. A diferença é que o nosso modelo dá à paciente controlo sobre quem vê o quê e quando — não apenas visibilidade.
Perguntas? Escreve para peter@medicus.global