Exportação de Dados Pessoais
Uma exportação de dados no estilo GDPR permite que os usuários baixem todos os seus dados pessoais. As principais preocupações são: exclusão de campos sensíveis do payload de exportação, tokens de download seguros e aplicação de expiração.
Componentes principais
- Job de exportação: um registro que vincula um usuário a um token de download opaco, com status (pending → ready) e um timestamp de expiração.
- Etapa de processamento: uma operação do lado do worker que constrói o payload e marca o job como pronto.
- Download: busca o payload pelo token, verificando a expiração antes de servir.
Schema
CREATE TABLE data_exports (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
token TEXT NOT NULL UNIQUE,
status TEXT NOT NULL DEFAULT 'pending',
payload TEXT,
expires_at TEXT NOT NULL,
created_at TEXT NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id)
);Geração do token
Use bin2hex(random_bytes(32)) — 64 caracteres hex, 256 bits de entropia. IDs sequenciais, timestamps ou tokens baseados em MD5 são adivináveis e não devem ser usados para tokens de download.
$token = bin2hex(random_bytes(32));Exclusão de campos sensíveis
O payload de exportação nunca deve conter credenciais ou campos para os quais o usuário não consentiu explicitamente a exportação. Exclua no nível do repositório, não na camada HTTP:
public function processExport(string $token, User $user, array $activities, string $now): DataExport
{
$payload = json_encode([
'exported_at' => $now,
'user' => [
'id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'created_at' => $user->createdAt,
// password_hash intencionalmente excluído
// phone intencionalmente excluído (requer reconsentimento para PII)
],
'activities' => $activities,
], JSON_THROW_ON_ERROR);
// ...
}Aplique a mesma exclusão ao endpoint de perfil público — phone, password_hash e quaisquer campos internos não devem aparecer nas respostas de GET /users/{id} também.
Aplicação da expiração
Aplique a expiração em ambos o endpoint de download e o endpoint de processamento:
// Em downloadExport:
if ($export->isExpired($now)) {
return $this->problems->create($request, 'gone', 'Export has expired.', 410, '');
}
// Em processExport — CRÍTICO: também verificar aqui
if ($export->isExpired($now)) {
return $this->problems->create($request, 'gone', 'Export request has expired. Please request a new export.', 410, '');
}Sem a verificação em processExport, um worker que recebe um job desatualizado escreveria os dados do usuário no banco mesmo após o fechamento da janela de download, criando registros órfãos com dados de payload sensíveis.
Fluxo de status
pending ──(processamento chamado, não expirado)──▶ ready ──(download chamado)──▶ [payload servido]
│ │
└──(processamento chamado, expirado)──▶ 410 └──(expirado)──▶ 410Download: 410 Gone vs 404 Not Found
- 404: o token não existe no banco de dados.
- 410 Gone: o token existe, mas expirou. Este é o status correto — o recurso existia e foi removido desde então. Os clientes podem usar esse sinal para solicitar ao usuário que faça uma nova exportação.
Decisões de design
Por que uma etapa process separada em vez de geração síncrona? Payloads de exportação podem ser grandes (anos de dados de atividade). A geração síncrona no handler HTTP corre risco de timeouts e ocupa um worker. O padrão assíncrono permite que o usuário solicite e verifique mais tarde. Para este FT, a etapa de processamento é exposta como uma API para simular a invocação do worker.
Por que usar o token como URL de download em vez do ID de exportação? Um ID inteiro sequencial é vulnerável a IDOR — o usuário 1 poderia baixar a exportação do usuário 2 incrementando o ID. Um token aleatório opaco torna a URL de download impossível de adivinhar.
A etapa process deve ser um endpoint público? Em produção, não. O endpoint de processamento deve ser chamado apenas por workers internos (via chave de API, rede interna ou fila). Neste FT está exposto para testabilidade. A entropia do token fornece alguma proteção, mas não é substituta para autenticação adequada de workers.