Skip to content
KitploitKITPLOIT
FerramentasBlog
Enviar
FerramentasBlog
Enviar

Ferramentas de Hacking, PenTest e Cibersegurança para o seu Arsenal de Segurança!

Kitploit é um diretório de ferramentas de hacking, cibersegurança e pentesting. Descubra as últimas atualizações de projetos para encontrar vulnerabilidades, analisar sistemas, automatizar testes e fortalecer sua segurança.

··Feeds·Contato·Privacidade·© 2026 Kitploit

Diretório de Ferramentas

Categorias

Ver todas as categorias
Loading categories
CVE-2026-19264 — CVE-2026-19264 - Travessia de caminho crítica não autenticada para assumir completamente a instância no Postiz (< 2.22.1). Análise técnica: bypass da ordem de decodificação, escalonamento de JWT_SECRET e análise da correção upstream. | Kitploit
Ferramentas/GitHubGitHub/darklycn1976/cve-2026-19264
Análise de VulnerabilidadesAnálise de CódigoExploraçãoExploração de Aplicações WebSegurança WebAprendizado e Educação
GitHubdarklycn1976/cve-2026-19264

CVE-2026-19264

CVE-2026-19264 - Travessia de caminho crítica não autenticada para assumir completamente a instância no Postiz (< 2.22.1). Análise técnica: bypass da ordem de decodificação, escalonamento de JWT_SECRET e análise da correção upstream.

Ver RepositórioSite
2há 1 mêsAinda não revisado

Mais Populares

Ver todos →

Descubra as ferramentas mais usadas pela nossa comunidade.

Explore todas as ferramentas

Navegue pela nossa coleção de ferramentas

Ver todas as ferramentas →
Compartilhar

CVE-2026-19264 - Path Traversal não autenticado para assunção total da instância no Postiz

CVE-2026-19264 - Path Traversal não autenticado para assunção total da instância no Postiz

Autor: Krithik Babu P (@DarkLycn1976) Publicado: 2026-08-10 CVE: CVE-2026-19264 Gravidade: Crítica - CVSS 4.0 9.3 / CVSS 3.1 9.8 CWE: CWE-22 - Limitação inadequada de nome de caminho a um diretório restrito Afetados: gitroomhq/postiz-app < 2.22.1 Corrigido em: v2.22.1


TL;DR

O Postiz servia mídias armazenadas localmente por meio de uma rota que concatenava segmentos de caminho fornecidos pela URL ao diretório de upload e transmitia o resultado de volta - sem normalização de caminho, sem verificação de contenção e sem autenticação.

O payload de traversal óbvio retorna , porque o Next.js colapsa os segmentos do roteamento. Mas e são decodificados exatamente mais uma vez no caminho até a chamada ao sistema de arquivos, restaurando a traversal do outro lado de todas as verificações.

404
../
antes
separadores codificados em URL sobrevivem à correspondência de rota

Um atacante não autenticado podia ler qualquer arquivo legível pelo processo da aplicação - incluindo seu próprio ambiente, que contém o segredo de assinatura JWT. Como o Postiz assina tokens de sessão com esse segredo e os emite sem uma declaração de expiração, recuperá-lo converte uma primitiva de leitura de arquivo em uma sessão permanente e forjável como qualquer usuário, incluindo um administrador.

Uma única requisição GET não autenticada para a assunção total da instância.


1. Contexto

Postiz é uma plataforma de agendamento de mídias sociais de código aberto - cerca de 34.000 estrelas no GitHub no momento em que este texto foi escrito - construída como um frontend Next.js com um backend NestJS. É amplamente auto-hospedado por agências e pequenas equipes para gerenciar contas sociais conectadas, conteúdo agendado e cobrança.

Implantações auto-hospedadas podem armazenar mídias enviadas localmente em vez de em armazenamento de objetos. Esse comportamento é controlado por uma única variável de ambiente:

root@kitploit:~
STORAGE_PROVIDER=local

Este é o valor incluído no .env.example, portanto é o que a maioria dos auto-hospedeiros executa, a menos que configurem deliberadamente S3 ou Cloudflare R2.

2. A superfície de ataque

Quando o armazenamento local está ativo, o next.config.js reescreve o caminho público /uploads/:path* para uma rota de API interna:

root@kitploit:~
apps/frontend/src/app/(app)/api/uploads/[[...path]]/route.ts

Duas propriedades tornam esta rota interessante antes mesmo de qualquer bug estar envolvido:

  1. Ela não é autenticada. Nenhum middleware de frontend a protege. Servir mídia pública é a intenção, então nenhuma sessão é necessária.
  2. Ela é um catch-all. O segmento catch-all opcional [[...path]] significa que cada componente de caminho restante chega como um array que o handler é livre para interpretar.

Quando STORAGE_PROVIDER é qualquer coisa diferente de local, a reescrita aponta para /404 e o handler fica inacessível. Essa barreira de configuração é a única coisa entre uma implantação e este bug.

3. O código vulnerável

O handler, antes da v2.22.1:

root@kitploit:~
export const GET = async (request: NextRequest, context) => {
  const { path } = await context.params;
  const filePath =
    process.env.UPLOAD_DIRECTORY + '/' + (path ?? []).join('/');

  const response  = createReadStream(filePath);
  const fileStats = statSync(filePath);
  // ... stream the file back to the caller
};

Três defeitos em quatro linhas:

  • Sem normalização. path.normalize(), path.resolve() - nenhum é chamado. Quaisquer segmentos que chegam são concatenados literalmente.
  • Sem verificação de contenção. Nada verifica se o filePath resultante ainda está dentro de UPLOAD_DIRECTORY.
  • Concatenação de strings, não junção de caminhos. + '/' + trata os componentes como texto, não como um caminho com semântica.

O resultado vai direto para createReadStream() e os bytes são transmitidos ao chamador com um tipo MIME inferido do nome do arquivo. Não há lista de permissões de extensões nem filtro de conteúdo.

4. Por que o payload óbvio falha

O ataque clássico é:

root@kitploit:~
GET /uploads/../../../etc/passwd

No Postiz isso retorna 404, e esse 404 é exatamente o motivo pelo qual este bug sobreviveu até ser encontrado.

O Next.js normaliza o caminho da requisição durante o roteamento. Segmentos crus de ../ são colapsados antes que o roteador decida qual handler invocar. Quando a requisição chega ao catch-all, a traversal já foi eliminada - ou o caminho resolve para algum lugar sem rota correspondente, ou resolve de volta dentro de /uploads com os segmentos de ponto removidos.

Para alguém testando rapidamente, esse 404 parece "o framework lida com isso". É uma defesa genuína e funcional. O problema não é que ela esteja ausente - é onde no pipeline ela é executada.

5. O bypass - uma incompatibilidade na ordem de decodificação

A correspondência de rota e o handler de requisição não executam o mesmo número de passagens de decodificação percentual.

Se os separadores forem codificados em percentual, a sequência não é um separador de caminho durante a correspondência de rota. %2e%2e%2f é apenas uma string opaca - texto inerte que o normalizador não tem motivo para tocar. Ela atravessa o roteamento intacta, é correspondida pelo catch-all e é decodificada no caminho para os params do handler, onde se torna ../ novamente.

Nesse ponto, ela é concatenada a UPLOAD_DIRECTORY e entregue a createReadStream() - depois do roteamento, depois da normalização, depois de todo controle que a teria impedido.

Formas funcionais:

root@kitploit:~
GET /uploads/%2e%2e%2fsecretdir%2fsecret.txt        → 200, file outside the upload directory
GET /uploads/..%2f..%2f..%2fetc%2fpasswd            → 200
GET /uploads/%2e%2e%2f%2e%2e%2f...%2fetc%2fpasswd   → 200, returned the real /etc/passwd

A dupla codificação não funciona - %252e permanece literal através da única passagem de decodificação e nunca se torna um ponto. Exatamente uma camada de codificação é o ponto ideal, o que é um lembrete útil de que "codificar mais forte" não é uma estratégia.

O invariante a reter:

Um controle que é executado antes que a decodificação esteja completa não está protegendo o sink.

6. Escalação - de leitura arbitrária a assunção da instância

Uma primitiva de leitura de arquivo é Alta por si só. O que torna isso Crítico é o que ela alcança.

Passo 1 - ler o ambiente. A configuração do próprio processo Node está em disco na raiz da implantação. O .env fornece, entre outras coisas:

  • JWT_SECRET - a chave de assinatura dos tokens de sessão
  • DATABASE_URL - credenciais completas do Postgres
  • Segredos OAuth dos provedores conectados e chaves de cobrança

Passo 2 - forjar uma sessão. O Postiz assina tokens de sessão com JWT_SECRET usando HS256 via jsonwebtoken. Criticamente, os tokens são emitidos sem expiresIn, então um token forjado é válido indefinidamente.

Passo 3 - tornar-se qualquer um. O middleware de autenticação re-resolve o usuário do banco de dados usando a declaração id. Ele deliberadamente não confia em uma declaração como isSuperAdmin do token - bom design - mas esse endurecimento é irrelevante quando você pode assinar um id arbitrário. Assinar { id: <victim user id> } produz uma sessão indistinguível de um login legítimo:

root@kitploit:~
read .env  →  JWT_SECRET  →  sign({ id: victim })  →  authenticated as victim, forever

Validei isso contra a lógica de verificação real do projeto com a dependência real jsonwebtoken: um token assinado com o segredo recuperado foi aceito, e o mesmo token assinado com um segredo errado foi rejeitado. O caso de controle importa - sem ele, você tem uma suposição, não uma descoberta.

Passo 4 - o caminho paralelo. DATABASE_URL sozinho é suficiente para acesso direto ao Postgres: leia todas as contas conectadas ou altere diretamente um flag de administrador.

Sem senha. Sem acesso prévio. Sem interação do usuário. Uma única requisição HTTP não autenticada.

7. Impacto e pontuação

root@kitploit:~
CVSS 4.0  9.3  AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N
CVSS 3.1  9.8  AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

PR:N e UI:N são as duas métricas que fazem o trabalho pesado. A rota não exige sessão nem interação da vítima - o atacante age sozinho, pela rede, contra uma configuração padrão.

O único limitador honesto é a barreira de configuração: implantações em S3 ou R2 não estão expostas, porque a rota reescreve para /404. Isso reduz a população afetada, mas não a gravidade para quem está dentro dela - e local é o padrão que acompanha o produto.

8. A correção

O patch dos mantenedores (7936062) tem oito linhas e vale a pena ser lido, porque é correto de uma maneira que essas correções frequentemente não são:

root@kitploit:~
+import { resolve, sep } from 'path';
...
-  const filePath =
-    process.env.UPLOAD_DIRECTORY + '/' + (path ?? []).join('/');
+  const base = resolve(process.env.UPLOAD_DIRECTORY!);
+  const filePath = resolve(base, (path ?? []).join('/'));
+  // Confine reads to UPLOAD_DIRECTORY. resolve() collapses any `..` segments
+  // (including URL-decoded ones), so this blocks every path-traversal variant.
+  if (filePath !== base && !filePath.startsWith(base + sep)) {
+    return new NextResponse('Not found', { status: 404 });
+  }

Duas coisas que ele acerta:

  1. Ele normaliza no sink. resolve() colapsa .. após a decodificação estar completa, então não importa como a traversal foi contrabandeada pelo roteamento. A verificação agora fica onde está o perigo.
  2. Ele compara com base + sep, não com base. Um filePath.startsWith(base) ingênuo aceitaria /app/uploads-evil/x como estando dentro de /app/uploads - um bypass clássico de correspondência de prefixo. Acrescentar o separador fecha isso, e a cláusula filePath !== base mantém o próprio diretório válido.

Essa é a forma correta para uma verificação de contenção: resolver e então comparar com a base usando um separador no final.

9. Linha do tempo da divulgação

Todos os horários em UTC, 2026-07-20, salvo indicação em contrário.

HorárioEvento
05:55Aviso relatado à equipe do Postiz
07:44Reconhecido e verificado pelos mantenedores
12:18Correção commitada, verificada e publicada
2026-08-07 14:13CVE-2026-19264 atribuído pelo Postiz (CNA)
2026-08-07 14:15GitHub Security Advisory publicado

Seis horas e vinte e três minutos do relato ao patch enviado, em um projeto de código aberto sem programa de bug bounty. Já tive relatos parados por meses em organizações com equipes de segurança dedicadas. Crédito a Enno Gelhaus pela coordenação e a Nevo David pela remediação.

10. Lições

Um controle que passa no seu teste não significa que o controle está no lugar certo. O 404 era real. O Next.js genuinamente colapsa ../. A defesa simplesmente foi executada antes que a entrada terminasse de ser decodificada, o que significava que ela protegia o roteador em vez da chamada ao sistema de arquivos. Quando você encontrar uma mitigação, pergunte quando ela é executada em relação ao sink - não apenas se ela existe.

Codificação é uma camada, e camadas são removidas em ritmos diferentes. Sempre que dois componentes em um pipeline de requisição discordam sobre quantas vezes decodificar, a lacuna entre eles é explorável. Correspondentes de rota, middlewares e handlers frequentemente discordam.

Classifique uma primitiva de leitura de arquivo pelo que o processo pode alcançar, não pela primitiva. "Leitura arbitrária de arquivo" parece divulgação de informações. Tornou-se Crítica porque o ambiente era legível, o segredo nele assinava sessões e essas sessões nunca expiravam. Siga a cadeia antes de pontuá-la.

Tokens sem expiração transformam um vazamento em um comprometimento permanente. A divulgação de uma chave de assinatura com tokens de curta duração é um dia ruim. Sem expiresIn, é irrecuperável sem rotacionar o segredo - e a maioria dos operadores nunca saberá que precisava.

Execute o caso de controle. Verificar que um token assinado com o segredo errado é rejeitado é o que separa uma descoberta demonstrada de uma suposta.

11. Referências

  • Registro CVE - https://www.cve.org/CVERecord?id=CVE-2026-19264
  • GitHub Security Advisory - https://github.com/gitroomhq/postiz-app/security/advisories/GHSA-4hgh-5rhf-4qpm
  • Aviso do CNA do Postiz (PSA-2026-TH12B7) - https://gadvisory.org/advisories/PSA-2026-TH12B7
  • Commit da correção - https://github.com/gitroomhq/postiz-app/commit/7936062
  • Versão corrigida v2.22.1 - https://github.com/gitroomhq/postiz-app/releases/tag/v2.22.1
  • CWE-22 - https://cwe.mitre.org/data/definitions/22.html

Orientação de remediação

Se você auto-hospeda o Postiz:

  1. Atualize para v2.22.1 ou posterior. Esta é a correção.
  2. Presuma que JWT_SECRET está comprometido se você executou uma versão afetada em um host publicamente acessível com STORAGE_PROVIDER=local. Rotacione-o. Como os tokens não têm expiração, a rotação é a única maneira de invalidar qualquer um que tenha sido forjado.
  3. Rotacione as credenciais de DATABASE_URL e quaisquer segredos OAuth dos provedores conectados mantidos no mesmo ambiente.
  4. Verifique os logs de acesso em busca de requisições GET para /uploads/ contendo %2e ou %2f.

Pesquisa conduzida de forma independente e divulgada ao fornecedor sob divulgação coordenada. Publicada após o patch ser enviado e o aviso se tornar público. Nenhum sistema de terceiros foi acessado - toda a validação foi realizada contra uma instância local construída a partir do código-fonte do próprio projeto.


Licença

Este writeup é licenciado sob CC BY 4.0 - compartilhe e adapte livremente com atribuição. Trechos de código de gitroomhq/postiz-app são citados para análise de segurança e permanecem sob a licença desse projeto.

Krithik Babu P - @DarkLycn1976

Baixar ferramenta