
CVE-2026-19264 - Postiz (< 2.22.1) में गंभीर अनप्रमाणित पाथ ट्रैवर्सल से पूर्ण इंस्टेंस टेकओवर। तकनीकी विवरण: डिकोड-ऑर्डर बायपास, JWT_SECRET एस्केलेशन, और अपस्ट्रीम फिक्स का विश्लेषण।

लेखक: कृथिक बाबू पी (@DarkLycn1976)
प्रकाशित: 2026-08-10
CVE: CVE-2026-19264
गंभीरता: क्रिटिकल - CVSS 4.0 9.3 / CVSS 3.1 9.8
CWE: CWE-22 - प्रतिबंधित निर्देशिका में पाथनाम की अनुचित सीमा
प्रभावित: gitroomhq/postiz-app < 2.22.1
सुधारित संस्करण: v2.22.1
Postiz स्थानीय रूप से संग्रहीत मीडिया को एक रूट के माध्यम से सर्व करता था जो URL-आपूर्ति किए गए पाथ सेगमेंट्स को अपलोड निर्देशिका के साथ जोड़ता था और परिणाम को वापस स्ट्रीम करता था - बिना किसी पाथ नॉर्मलाइज़ेशन, बिना किसी कंटेनमेंट जाँच, और बिना किसी प्रमाणीकरण के।
स्पष्ट ट्रैवर्सल पेलोड 404 लौटाता है, क्योंकि Next.js रूटिंग से पहले ../ सेगमेंट्स को समाप्त कर देता है। लेकिन URL-एन्कोडेड सेपरेटर रूट मैचिंग से बच जाते हैं और फ़ाइलसिस्टम कॉल के रास्ते में ठीक एक बार और डीकोड किए जाते हैं, हर जाँच के दूसरी ओर ट्रैवर्सल को पुनर्स्थापित करते हुए।
एक अनप्रमाणित हमलावर एप्लिकेशन प्रोसेस द्वारा पठनीय किसी भी फ़ाइल को पढ़ सकता था - जिसमें उसका अपना एनवायरनमेंट भी शामिल है, जिसमें JWT साइनिंग सीक्रेट होता है। क्योंकि Postiz उस सीक्रेट के साथ सत्र टोकन पर हस्ताक्षर करता है और उन्हें बिना किसी समाप्ति (expiry) क्लेम के जारी करता है, इसे प्राप्त करना एक फ़ाइल-रीड प्रिमिटिव को किसी भी उपयोगकर्ता के रूप में स्थायी, जाली बनाने योग्य सत्र में बदल देता है, जिसमें एक व्यवस्थापक भी शामिल है।
पूर्ण इंस्टेंस टेकओवर के लिए केवल एक अनप्रमाणित GET अनुरोध।
Postiz एक ओपन-सोर्स सोशल मीडिया शेड्यूलिंग प्लेटफ़ॉर्म है - लिखने के समय लगभग 34,000 GitHub स्टार्स - जो Next.js फ्रंटएंड और NestJS बैकएंड के रूप में बनाया गया है। यह कनेक्टेड सोशल अकाउंट्स, शेड्यूल किए गए कंटेंट और बिलिंग को प्रबंधित करने के लिए एजेंसियों और छोटी टीमों द्वारा व्यापक रूप से सेल्फ-होस्ट किया जाता है।
सेल्फ-होस्टेड डिप्लॉयमेंट अपलोड किए गए मीडिया को ऑब्जेक्ट स्टोरेज के बजाय स्थानीय रूप से संग्रहीत कर सकते हैं। यह व्यवहार एक ही एनवायरनमेंट वेरिएबल द्वारा नियंत्रित होता है:
STORAGE_PROVIDER=local
यही वह मान है जो .env.example में भेजा जाता है, इसलिए जब तक वे जानबूझकर S3 या Cloudflare R2 कॉन्फ़िगर नहीं करते, अधिकांश सेल्फ-होस्टर यही चलाते हैं।
जब स्थानीय स्टोरेज सक्रिय होता है, next.config.js सार्वजनिक पाथ /uploads/:path* को एक आंतरिक API रूट पर रीराइट करता है:
apps/frontend/src/app/(app)/api/uploads/[[...path]]/route.ts
कोई बग शामिल होने से पहले ही दो गुण इस रूट को दिलचस्प बनाते हैं:
[[...path]] वैकल्पिक कैच-ऑल सेगमेंट का अर्थ है कि शेष हर पाथ घटक एक ऐरे के रूप में आता है जिसे हैंडलर अपने अनुसार व्याख्या करने के लिए स्वतंत्र है।जब STORAGE_PROVIDER local के अलावा कुछ भी होता है, तो रीराइट /404 की ओर इंगित करता है और हैंडलर अगम्य हो जाता है। वह कॉन्फ़िगरेशन गेट ही एकमात्र चीज़ है जो किसी डिप्लॉयमेंट और इस बग के बीच खड़ी है।
v2.22.1 से पहले का हैंडलर:
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
};
चार पंक्तियों में तीन दोष:
path.normalize(), path.resolve() - इनमें से कोई भी कॉल नहीं किया जाता। जो भी सेगमेंट आते हैं, वे यथावत जोड़ दिए जाते हैं।filePath अभी भी UPLOAD_DIRECTORY के अंदर है।+ '/' + घटकों को टेक्स्ट के रूप में मानता है, न कि अर्थ-सहित पाथ के रूप में।परिणाम सीधे createReadStream() में जाता है और बाइट्स को फ़ाइलनाम से अनुमानित MIME प्रकार के साथ कॉलर को स्ट्रीम किया जाता है। एक्सटेंशन की कोई अनुमति-सूची (allow-list) नहीं है और कोई कंटेंट फ़िल्टर नहीं है।
पाठ्यपुस्तकीय (textbook) हमला है:
GET /uploads/../../../etc/passwd
Postiz पर यह 404 लौटाता है, और यही 404 पूरा कारण है कि यह बग मिलने तक जीवित रहा।
Next.js रूटिंग के दौरान अनुरोध पाथ को नॉर्मलाइज़ करता है। रूटर यह तय करने से पहले कि किस हैंडलर को आमंत्रित किया जाए, कच्चे ../ सेगमेंट्स को समाप्त कर दिया जाता है। जब तक अनुरोध कैच-ऑल तक पहुँचता है, ट्रैवर्सल पहले ही समाप्त किया जा चुका होता है - या तो पाथ कहीं ऐसी जगह रिज़ॉल्व होता है जहाँ कोई मेल खाने वाला रूट नहीं होता, या यह डॉट-सेगमेंट्स के बिना वापस /uploads के अंदर रिज़ॉल्व होता है।
जल्दी से परीक्षण करने वाले किसी व्यक्ति के लिए, वह 404 "फ्रेमवर्क इसे संभाल लेता है" जैसा पढ़ा जाता है। यह एक वास्तविक, कार्यशील सुरक्षा है। समस्या यह नहीं है कि यह अनुपस्थित है - समस्या यह है कि यह पाइपलाइन में कहाँ चलता है।
रूट मैचिंग और अनुरोध हैंडलर समान संख्या में पर्सेंट-डीकोडिंग पास नहीं करते।
यदि सेपरेटर पर्सेंट-एन्कोडेड हैं, तो अनुक्रम रूट मैचिंग के दौरान पाथ सेपरेटर नहीं होता। %2e%2e%2f केवल एक अपारदर्शी स्ट्रिंग है - निष्क्रिय टेक्स्ट जिसे नॉर्मलाइज़र छूने का कोई कारण नहीं रखता। यह रूटिंग से सुरक्षित गुज़रता है, कैच-ऑल द्वारा मैच किया जाता है, और हैंडलर के params में जाते समय डीकोड हो जाता है, जहाँ यह फिर से ../ बन जाता है।
उस बिंदु पर इसे UPLOAD_DIRECTORY के साथ जोड़ दिया जाता है और createReadStream() को सौंप दिया जाता है - रूटिंग से परे, नॉर्मलाइज़ेशन से परे, हर उस नियंत्रण से परे जो इसे रोक सकता था।
कार्यशील रूप:
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
डबल-एन्कोडिंग काम नहीं करती - %252e एकल डीकोड पास के माध्यम से शाब्दिक बना रहता है और कभी डॉट नहीं बनता। ठीक एक परत एन्कोडिंग सबसे उपयुक्त है, जो एक उपयोगी अनुस्मारक है कि "और कठिन एन्कोड करो" कोई रणनीति नहीं है।
जिस सिद्धांत (invariant) को धारण करना है:
डीकोडिंग पूर्ण होने से पहले चलने वाला नियंत्रण सिंक (sink) की सुरक्षा नहीं कर रहा है।
फ़ाइल-रीड प्रिमिटिव अपने आप में High है। इसे Critical बनाने वाली चीज़ है कि यह किस तक पहुँचता है।
चरण 1 - एनवायरनमेंट पढ़ें। Node प्रोसेस का अपना कॉन्फ़िगरेशन डिप्लॉयमेंट रूट में डिस्क पर होता है। .env अन्य चीज़ों के अलावा यह देता है:
JWT_SECRET - सत्र टोकन हस्ताक्षर कुंजीDATABASE_URL - पूर्ण Postgres क्रेडेंशियल्सचरण 2 - सत्र जाली बनाएँ। Postiz jsonwebtoken के माध्यम से HS256 का उपयोग करके JWT_SECRET के साथ सत्र टोकन पर हस्ताक्षर करता है। महत्वपूर्ण रूप से, टोकन बिना किसी expiresIn के जारी किए जाते हैं, इसलिए एक जाली टोकन अनिश्चित काल तक मान्य रहता है।
चरण 3 - कोई भी बन जाएँ। प्रमाणीकरण मिडलवेयर id क्लेम का उपयोग करके डेटाबेस से उपयोगकर्ता को पुनः-रिज़ॉल्व करता है। यह जानबूझकर टोकन से isSuperAdmin जैसे क्लेम पर भरोसा नहीं करता - अच्छा डिज़ाइन - लेकिन एक बार जब आप किसी भी id पर हस्ताक्षर कर सकते हैं तो वह सख्ती अप्रासंगिक हो जाती है। { id: <victim user id> } पर हस्ताक्षर करने से एक ऐसा सत्र उत्पन्न होता है जो वैध लॉगिन से अप्रभेद्य होता है:
read .env → JWT_SECRET → sign({ id: victim }) → authenticated as victim, forever