● CWE-22 · CVE-2007-4559

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 का tarfile module 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 की संख्या और कुल आकार गिनें।

हमले का चरण-दर-चरण प्रवाह

चरण 1

ऐप अपलोड किया गया archive स्वीकार करता है

data import, plugin installer या CI artifact handler किसी यूज़र से .tar.gz पाता है।

चरण 2

एक member का नाम folder के बाहर इशारा करता है

किसी entry का सहेजा गया path destination से ऊपर निकल जाता है या absolute होता है। ../outside.txt नाम के member वाला टेस्ट archive यह मामला दिखाता है।

चरण 3

extractall resolve हुए path पर लिखता है

फ़ाइल वहीं पहुँचती है जहाँ नाम resolve होता है, और destination ख़ुद भी यूज़र के दिए job_id से आ सकता है।

चरण 4

ऐप्लिकेशन की फ़ाइलें बदल दी जाती हैं

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

इंजीनियरिंग और सिस्टम सुरक्षा चेकलिस्ट

स्रोत

← पूरी सुरक्षा निर्देशिका देखें सभी कमज़ोरी गाइड →