Skip to content
KitploitKITPLOIT
StrumentiBlog
Invia
StrumentiBlog
Invia

Strumenti di Hacking, PenTest e Cybersecurity per il tuo Arsenale di Sicurezza!

Kitploit è una directory di strumenti di hacking, cybersecurity e pentesting. Scopri gli ultimi aggiornamenti dei progetti per trovare vulnerabilità, analizzare sistemi, automatizzare i test e rafforzare la tua sicurezza.

··Feed·Contatto·Privacy·© 2026 Kitploit

Directory degli strumenti

Categorie

Vedi tutte le categorie
Loading categories
CVE-2026-19264 — CVE-2026-19264 - Path traversal critico non autenticato fino alla compromissione completa dell'istanza in Postiz (< 2.22.1). Writeup tecnico: bypass dell'ordine di decodifica, escalation di JWT_SECRET e analisi della correzione a monte. | Kitploit
Strumenti/GitHubGitHub/darklycn1976/cve-2026-19264
Analisi delle VulnerabilitàAnalisi del CodiceExploitSfruttamento di Applicazioni WebSicurezza WebApprendimento e Formazione
GitHubdarklycn1976/cve-2026-19264

CVE-2026-19264

CVE-2026-19264 - Path traversal critico non autenticato fino alla compromissione completa dell'istanza in Postiz (< 2.22.1). Writeup tecnico: bypass dell'ordine di decodifica, escalation di JWT_SECRET e analisi della correzione a monte.

Vedi RepositorySito web
21 mese faNon ancora revisionato

Più Popolari

Vedi tutti →

Scopri gli strumenti più utilizzati dalla nostra community.

Esplora tutti gli strumenti

Sfoglia la nostra collezione di strumenti

Vedi tutti gli strumenti →
Condividi

CVE-2026-19264 - Unauthenticated Path Traversal to Full Instance Takeover in Postiz

CVE-2026-19264 - Path Traversal non autenticato che porta alla compromissione totale dell'istanza in Postiz

Autore: Krithik Babu P (@DarkLycn1976) Pubblicato: 2026-08-10 CVE: CVE-2026-19264 Gravità: Critica - CVSS 4.0 9.3 / CVSS 3.1 9.8 CWE: CWE-22 - Limitazione impropria di un pathname a una directory ristretta Versioni affette: gitroomhq/postiz-app < 2.22.1 Corretta in: v2.22.1


TL;DR

Postiz serviva i media archiviati localmente tramite una route che univa i segmenti di percorso forniti dall'URL alla directory di upload e trasmetteva il risultato al chiamante - senza normalizzazione del percorso, senza controllo di contenimento e senza autenticazione.

Il payload di traversal più ovvio restituisce 404, perché Next.js compatta i segmenti ../ prima del routing. Ma i separatori codificati in URL sopravvivono al matching delle route e vengono decodificati esattamente un'ultima volta lungo il percorso verso la chiamata al filesystem, ripristinando la traversal al di là di ogni controllo.

Un attaccante non autenticato poteva leggere qualsiasi file leggibile dal processo dell'applicazione - incluso il suo stesso ambiente, che contiene il segreto di firma JWT. Poiché Postiz firma i token di sessione con quel segreto e li emette senza un claim di scadenza, il suo recupero trasforma una primitiva di lettura file in una sessione permanente e falsificabile come qualsiasi utente, incluso un amministratore.

Una richiesta GET non autenticata fino alla compromissione totale dell'istanza.


1. Contesto

Postiz è una piattaforma open-source di pianificazione per i social media - circa 34.000 stelle su GitHub al momento della stesura - realizzata come frontend Next.js con backend NestJS. È ampiamente auto-ospitata da agenzie e piccoli team per gestire account social connessi, contenuti pianificati e fatturazione.

Le distribuzioni self-hosted possono archiviare i media caricati localmente anziché su un object storage. Questo comportamento è controllato da una singola variabile d'ambiente:

root@kitploit:~
STORAGE_PROVIDER=local

Questo è il valore incluso in .env.example, quindi è ciò che eseguono la maggior parte degli auto-ospitanti a meno che non configurino deliberatamente S3 o Cloudflare R2.

2. La superficie d'attacco

Quando lo storage locale è attivo, next.config.js riscrive il percorso pubblico /uploads/:path* su una route API interna:

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

Due proprietà rendono questa route interessante prima ancora che entri in gioco un bug:

  1. Non è autenticata. Nessun middleware del frontend la protegge. Servire media pubblici è lo scopo, quindi non è richiesta alcuna sessione.
  2. È un catch-all. Il segmento catch-all opzionale [[...path]] fa sì che ogni componente rimanente del percorso arrivi come un array che l'handler è libero di interpretare.

Quando STORAGE_PROVIDER è un valore diverso da local, la riscrittura punta a /404 e l'handler è irraggiungibile. Quel gate di configurazione è l'unica cosa che separa una distribuzione da questo bug.

3. Il codice vulnerabile

L'handler, prima della 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
};

Tre difetti in quattro righe:

  • Nessuna normalizzazione. path.normalize(), path.resolve() - nessuno dei due viene chiamato. Qualsiasi segmento arrivi viene concatenato così com'è.
  • Nessun controllo di contenimento. Nulla verifica che il filePath risultante si trovi ancora all'interno di UPLOAD_DIRECTORY.
  • Concatenazione di stringhe, non unione di percorsi. + '/' + tratta i componenti come testo, non come un percorso con una semantica.

Il risultato finisce direttamente in createReadStream() e i byte vengono trasmessi al chiamante con un tipo MIME dedotto dal nome del file. Non esiste un'allow-list di estensioni né un filtro sui contenuti.

4. Perché il payload più ovvio fallisce

L'attacco da manuale è:

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

Su Postiz questo restituisce 404, e quel 404 è l'intera ragione per cui questo bug è sopravvissuto fino a quando non è stato trovato.

Next.js normalizza il percorso della richiesta durante il routing. I segmenti ../ grezzi vengono compattati prima che il router decida quale handler invocare. Quando la richiesta raggiunge il catch-all, la traversal è già stata eliminata - o il percorso risolve da qualche parte senza una route corrispondente, oppure risolve nuovamente all'interno di /uploads senza più i segmenti ...

Per chi sta testando rapidamente, quel 404 si legge come "il framework ci pensa lui". È una difesa genuina e funzionante. Il problema non è che sia assente - è dove nella pipeline viene eseguita.

5. Il bypass - una discrepanza nell'ordine di decodifica

Il matching delle route e l'handler della richiesta non eseguono lo stesso numero di passaggi di percent-decoding.

Se i separatori sono percent-encoded, la sequenza non è un separatore di percorso durante il matching delle route. %2e%2e%2f è solo una stringa opaca - testo inerte che il normalizzatore non ha alcun motivo di toccare. Attraversa il routing intatta, viene fatta corrispondere dal catch-all e viene decodificata mentre entra nei params dell'handler, dove torna a essere ../.

A quel punto viene concatenata a UPLOAD_DIRECTORY e passata a createReadStream() - oltre il routing, oltre la normalizzazione, oltre ogni controllo che l'avrebbe fermata.

Forme funzionanti:

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

Il doppio encoding non funziona - %252e rimane letterale attraverso il singolo passaggio di decodifica e non diventa mai un punto. Esattamente uno strato di encoding è il punto ottimale, un utile promemoria che "codificare di più" non è una strategia.

L'invariante da tenere a mente:

Un controllo eseguito prima che la decodifica sia completa non sta proteggendo il sink.

6. Escalation - dalla lettura arbitraria alla compromissione totale dell'istanza

Una primitiva di lettura file è di per sé di gravità Alta. Ciò che la rende Critica è ciò che riesce a raggiungere.

Passo 1 - leggere l'ambiente. La configurazione stessa del processo Node è su disco nella root della distribuzione. .env fornisce, tra le altre cose:

  • JWT_SECRET - la chiave di firma dei token di sessione
  • DATABASE_URL - credenziali Postgres complete
  • Segreti OAuth dei provider connessi e chiavi di fatturazione

Passo 2 - falsificare una sessione. Postiz firma i token di sessione con JWT_SECRET usando HS256 tramite jsonwebtoken. In modo critico, i token vengono emessi senza expiresIn, quindi un token falsificato è valido indefinitamente.

Passo 3 - diventare chiunque. Il middleware di autenticazione risolve nuovamente l'utente dal database usando il claim id. Deliberatamente non si fida di un claim come isSuperAdmin proveniente dal token - buona progettazione - ma quell'indurimento è irrilevante una volta che puoi firmare un id arbitrario. Firmare { id: <victim user id> } produce una sessione indistinguibile da un login legittimo:

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

Ho validato questo aspetto contro la logica di verifica reale del progetto con la dipendenza jsonwebtoken effettiva: un token firmato con il segreto recuperato è stato accettato, mentre lo stesso token firmato con un segreto sbagliato è stato rifiutato. Il caso di controllo conta - senza di esso hai un'ipotesi, non un riscontro.

Passo 4 - il percorso parallelo. La sola DATABASE_URL è sufficiente per l'accesso diretto a Postgres: leggere ogni account connesso oppure attivare direttamente un flag di amministratore.

Nessuna password. Nessun accesso precedente. Nessuna interazione con l'utente. Una richiesta HTTP non autenticata.

7. Impatto e punteggio

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 sono le due metriche che fanno il lavoro. La route non richiede alcuna sessione né interazione con la vittima - l'attaccante agisce da solo, attraverso la rete, contro una configurazione predefinita.

L'unico limite onesto è il gate di configurazione: le distribuzioni su S3 o R2 non sono esposte, perché la route riscrive su /404. Questo riduce la popolazione interessata ma non la gravità per chiunque ne faccia parte - e local è il default distribuito.

8. Il fix

Il patch dei maintainer (7936062) è di otto righe e merita di essere letto, perché è corretto in un modo in cui questi fix spesso non lo sono:

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 });
+  }

Due cose che fa bene:

  1. Normalizza al sink. resolve() compatta .. dopo che la decodifica è completa, quindi non importa come la traversal sia stata fatta passare di nascosto attraverso il routing. Il controllo ora si trova esattamente dove c'è il pericolo.
  2. Confronta con base + sep, non con base. Un ingenuo filePath.startsWith(base) accetterebbe /app/uploads-evil/x come se fosse all'interno di /app/uploads - un classico bypass del prefix match. Aggiungere il separatore chiude la falla, e la clausola filePath !== base mantiene valida la directory stessa.

Questa è la forma corretta per un controllo di contenimento: risolvere, poi confrontare con la base includendo un separatore finale.

9. Cronologia della divulgazione

Tutti gli orari sono in UTC, 2026-07-20, salvo diversa indicazione.

OraEvento
05:55Advisory segnalato al team di Postiz
07:44Riconosciuto e verificato dai maintainer
12:18Fix committato, verificato e pubblicato
2026-08-07 14:13CVE-2026-19264 assegnato da Postiz (CNA)
2026-08-07 14:15Pubblicato l'advisory di sicurezza GitHub

Sei ore e ventitré minuti dalla segnalazione al patch rilasciato, su un progetto open-source senza alcun bug bounty. Ho visto segnalazioni rimanere intatte per mesi presso organizzazioni con team di sicurezza dedicati. Il merito va a Enno Gelhaus per il coordinamento e a Nevo David per la remediation.

10. Punti chiave

Un controllo che supera il tuo test non significa che il controllo sia nel posto giusto. Il 404 era reale. Next.js compatta davvero ../. La difesa veniva semplicemente eseguita prima che l'input finisse di essere decodificato, il che significava che proteggeva il router piuttosto che la chiamata al filesystem. Quando trovi una mitigazione, chiediti quando viene eseguita rispetto al sink - non solo se esiste.

L'encoding è uno strato, e gli strati vengono rimossi a velocità diverse. Ogni volta che due componenti nella pipeline di una richiesta non concordano su quante volte decodificare, il divario tra loro è sfruttabile. I matcher delle route, il middleware e gli handler spesso non concordano.

Valuta una primitiva di lettura file in base a ciò che il processo può raggiungere, non alla primitiva. La "lettura arbitraria di file" suona come divulgazione di informazioni. È diventata Critica perché l'ambiente era leggibile, il segreto in esso contenuto firmava le sessioni e quelle sessioni non scadevano mai. Segui la catena prima di assegnare un punteggio.

I token senza scadenza trasformano una fuga in una compromissione permanente. La divulgazione di una chiave di firma con token a breve scadenza è una brutta giornata. Senza expiresIn, non è recuperabile senza ruotare il segreto - e la maggior parte degli operatori non saprà mai di averne bisogno.

Esegui il caso di controllo. Verificare che un token firmato con il segreto sbagliato venga rifiutato è ciò che separa un riscontro dimostrato da uno presunto.

11. Riferimenti

  • Record CVE - https://www.cve.org/CVERecord?id=CVE-2026-19264
  • Advisory di sicurezza GitHub - https://github.com/gitroomhq/postiz-app/security/advisories/GHSA-4hgh-5rhf-4qpm
  • Advisory CNA di Postiz (PSA-2026-TH12B7) - https://gadvisory.org/advisories/PSA-2026-TH12B7
  • Commit del fix - https://github.com/gitroomhq/postiz-app/commit/7936062
  • Release corretta v2.22.1 - https://github.com/gitroomhq/postiz-app/releases/tag/v2.22.1
  • CWE-22 - https://cwe.mitre.org/data/definitions/22.html

Indicazioni per la remediation

Se auto-ospiti Postiz:

  1. Aggiorna alla v2.22.1 o successiva. Questa è la correzione.
  2. Considera JWT_SECRET compromesso se hai eseguito una versione vulnerabile su un host raggiungibile pubblicamente con STORAGE_PROVIDER=local. Ruotalo. Poiché i token non hanno scadenza, la rotazione è l'unico modo per invalidare quelli eventualmente falsificati.
  3. Ruota le credenziali di DATABASE_URL e qualsiasi segreto OAuth dei provider connessi presenti nello stesso ambiente.
  4. Controlla i log di accesso per richieste GET a /uploads/ contenenti %2e o %2f.

Ricerca condotta in modo indipendente e divulgata al vendor nell'ambito di una coordinated disclosure. Pubblicata dopo la distribuzione del fix e la pubblicazione dell'advisory. Nessun sistema di terze parti è stato acceduto: tutta la validazione è stata eseguita su un'istanza locale costruita dal codice sorgente del progetto.


Licenza

Questo writeup è concesso in licenza CC BY 4.0 - condividi e adatta liberamente con attribuzione. Gli estratti di codice da gitroomhq/postiz-app sono citati per l'analisi di sicurezza e rimangono sotto la licenza di quel progetto.

Krithik Babu P - @DarkLycn1976

Scarica lo strumento