Python tarfile.extractall में पाथ ट्रैवर्सल (CVE-2007-4559): यह कैसे होता है और इसे कैसे ठीक करें
extraction filters आने से पहले tarfile.extractall() हर member को archive में सहेजे गए path पर लिखता था, इसलिए .. वाले नाम, absolute paths या links फ़ाइलों को target folder के बाहर रख सकते थे। PEP 706 का filter="data" argument इसे कैसे रोकता है, destination यूज़र इनपुट से क्यों नहीं आना चाहिए, और कौन-सी आकार सीमाएँ जोड़नी चाहिए, Python के उदाहरण के साथ।
आसान भाषा में (ELI5)
सामान शिफ़्ट करने वाली कंपनी आपके नए फ़्लैट में डिब्बे खोलती है और हर चीज़ वहाँ रखती है जहाँ उसका लेबल कहता है। 'रसोई, फिर मुख्य दरवाज़े से बाहर, फिर पड़ोसी के बेडरूम में' लिखा लेबल भी हूबहू माना जाता है, क्योंकि मज़दूर केवल लेबल पढ़ते हैं और कभी नहीं जाँचते कि पता अब भी आपके फ़्लैट के अंदर है या नहीं। archive भी ऐसे ही काम करता है: हर फ़ाइल अपना destination path साथ लाती है। उपाय एक फ़ोरमैन है जो पहले हर लेबल पढ़े और फ़्लैट से बाहर जाने वाली हर चीज़ ठुकरा दे, साथ ही हर वह चीज़ भी जो साधारण डिब्बा नहीं है, जैसे कहीं और इशारा करने वाला बोर्ड।
इस पेज के मुख्य शब्द
- CVE-2007-4559
- Python का
tarfilemodule member नामों को extraction directory से बाहर जाने से नहीं रोकता था। यह समस्या 2007 से ज्ञात थी, और 2022 में Trellix ने अनुमान लगाया कि लगभग 3.5 लाख open-source projects अविश्वसनीय archives परextractall()चलाते थे। - .. या absolute path वाले member नाम
../outside.txtया/etc/cron.d/jobनाम की tar entry destination से जोड़ने पर उसके बाहर पहुँचती है। filter के बिनाextractall()process की अनुमतियों के साथ उसी path पर लिख देता है।- Symbolic और hard link members
- archive पहले destination के अंदर कहीं और इशारा करने वाला link बना सकता है, फिर उसके ज़रिए एक साधारण फ़ाइल लिख सकता है। केवल नाम जाँचने से यह पकड़ में नहीं आता, इसीलिए filters links के target भी जाँचते हैं।
- Extraction filters (PEP 706)
filter="data"absolute नाम,..से बाहर निकलना, destination से बाहर जाने वाले links और device files ठुकराता है, और set-user-ID bits तथा दूसरों के लिए execute अनुमति हटा देता है। यह Python 3.12 से और 3.8 से 3.11 की security releases में उपलब्ध है, और Python 3.14 में डिफ़ॉल्ट बन जाता है।- Resource सीमाएँ
- filters आकार सीमित नहीं करते। एक छोटा compressed archive लाखों members या कई गीगाबाइट में फैल सकता है, इसलिए extract करने से पहले members की संख्या और कुल आकार गिनें।
हमले का चरण-दर-चरण प्रवाह
ऐप अपलोड किया गया archive स्वीकार करता है
data import, plugin installer या CI artifact handler किसी यूज़र से .tar.gz पाता है।
एक member का नाम folder के बाहर इशारा करता है
किसी entry का सहेजा गया path destination से ऊपर निकल जाता है या absolute होता है। ../outside.txt नाम के member वाला टेस्ट archive यह मामला दिखाता है।
extractall resolve हुए path पर लिखता है
फ़ाइल वहीं पहुँचती है जहाँ नाम resolve होता है, और destination ख़ुद भी यूज़र के दिए job_id से आ सकता है।
ऐप्लिकेशन की फ़ाइलें बदल दी जाती हैं
overwrite हुई configuration, templates या scripts अगली बार ऐप्लिकेशन के लोड करने पर चलती हैं, और इसी तरह यह path traversal code execution बन जाता है।
सोर्स कोड: कमज़ोर बनाम सुरक्षित कार्यान्वयन
# imports.py: हर member वहीं जाता है जहाँ उसका सहेजा गया नाम इशारा करे
import tarfile
from pathlib import Path
UPLOAD_ROOT = Path("/srv/app/imports")
# folder caller चुनता है, न filter, न आकार सीमा
def import_bundle(archive_path: str, job_id: str) -> Path:
dest = UPLOAD_ROOT / job_id
dest.mkdir(parents=True, exist_ok=True)
with tarfile.open(archive_path) as tar:
tar.extractall(dest)
return dest
# imports.py: data filter, सर्वर द्वारा नामित folder और आकार सीमाएँ
import tarfile
import uuid
from pathlib import Path
UPLOAD_ROOT = Path("/srv/app/imports")
MAX_MEMBERS = 10_000
MAX_TOTAL_BYTES = 500 * 1024 * 1024
# बहुत ज़्यादा members या bytes वाले archives ठुकराएँ
def check_limits(tar: tarfile.TarFile) -> None:
members = tar.getmembers()
if len(members) > MAX_MEMBERS or sum(m.size for m in members) > MAX_TOTAL_BYTES:
raise ValueError("archive too large")
# filter='data' .., absolute नाम, बाहर जाने वाले links और device files रोकता है
def import_bundle(archive_path: str) -> Path:
dest = UPLOAD_ROOT / uuid.uuid4().hex
dest.mkdir(parents=True)
with tarfile.open(archive_path, mode="r:*") as tar:
check_limits(tar)
tar.extractall(dest, filter="data")
return dest
इंजीनियरिंग और सिस्टम सुरक्षा चेकलिस्ट
- हर
extractall()औरextract()call मेंfilter="data"दें, और जब Bandit B202 या कोई Semgrep नियमfilter=के बिना call पाए तो CI फ़ेल करें। - सर्वर द्वारा नामित नए folder में extract करें, जैसे UUID, कभी रिक्वेस्ट डेटा से बने path में नहीं।
- extract करने से पहले members की संख्या और कुल uncompressed आकार जाँचें, और सीमा से बड़े archives ठुकराएँ।
- extraction worker को ऐसे यूज़र से चलाएँ जिसके पास ऐप्लिकेशन कोड या configuration पर लिखने की अनुमति न हो।
- CI में
../outside.txtऔर एक absolute path member वाले archive का टेस्ट जोड़ें, औरtarfile.FilterErrorकी अपेक्षा करें।