2025, Sep 24 21:31
Checkmarx/SAST में URL इंटरपोलेशन से होने वाले SSRF फ़्लैग का समाधान
Checkmarx जैसे SAST टूल्स में URL में UUID इंटरपोलेशन से ट्रिगर हुए SSRF फ़्लैग को बिना व्यवहार बदले हटाएँ। सुरक्षित URL-बिल्डिंग और Session आधारित दो पैटर्न सीखें.
Checkmarx जैसे स्टैटिक विश्लेषण टूल्स जोखिम भरे टेंट फ्लो को पहचानने में अच्छे होते हैं, और जैसे ही उपयोगकर्ता इनपुट किसी URL में इंटरपोलेट होता है, वे CI/CD पाइपलाइन रोक देते हैं। एक आम स्थिति है किसी निर्दोष दिखने वाले पहचानकर्ता से रिक्वेस्ट पाथ बनाना। सख्त UUID जांच के बाद भी, किसी वैरिएबल को सीधे URL पाथ में रखना संभावित SSRF के रूप में फ़्लैग हो सकता है। नीचे एक छोटा, वास्तविक पैटर्न दिया गया है जो ऐसी रिपोर्ट ट्रिगर करता है—और बिना एप्लिकेशन का व्यवहार बदले इसे कैसे संभालें।
समस्या का संदर्भ
एप्लिकेशन एक आंतरिक REST एंडपॉइंट से UUIDv4 के आधार पर फ़ाइल लाने के लिए क्वेरी करता है। होस्टनेम कॉन्फ़िगरेशन से आता है, पहचानकर्ता कड़ाई से वैलिडेट होता है, और फिर URL बनाने में उपयोग किया जाता है। इन जांचों के बावजूद, सीधा स्ट्रिंग इंटरपोलेशन CI/CD गेट में SSRF जोखिम के रूप में रिपोर्ट होता है।
import os
import re
import uuid
import validators
import requests
UUID_V4_PATTERN = r'^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'
def grab_document(doc_key):
    if not re.match(UUID_V4_PATTERN, doc_key):
        raise ValueError('invalid file_id')
    if not validators.uuid(doc_key):
        raise ValueError('invalid file_id')
    try:
        doc_key = str(uuid.UUID(doc_key))
    except Exception:
        raise ValueError('invalid file_id')
    svc_host = os.getenv('HOSTNAME', None)
    if svc_host is None:
        raise ValueError('invalid file_id')
    endpoint = f'https://{svc_host}/v1/files/{doc_key}'
    resp = requests.get(endpoint)
    if resp.status_code == 200:
        pass
यह फ़्लैग क्यों होता है
स्कैनर के दृष्टिकोण से, उपयोगकर्ता-नियंत्रित मान एक URL स्ट्रिंग तक पहुँचता है, जिसे बाद में HTTP रिक्वेस्ट में इस्तेमाल किया जाता है। यह डायरेक्ट फ्लो एक क्लासिक SSRF पैटर्न है। भले ही कोड यह सत्यापित करता है कि मान UUIDv4 है, पाथ निर्माण स्वयं एक टेंट सिंक बना रहता है। मुद्दा UUID फॉर्मैट नहीं, बल्कि बाहरी इनपुट को रिक्वेस्ट URL में इंटरपोलेट करना है।
दो सुरक्षित निर्माण पैटर्न
कोड को इस तरह पुनर्संरचित करने के दो सीधे तरीके हैं कि टेंट फ्लो टूट जाए और रिक्वेस्ट नियंत्रित ढंग से बने। दोनों में वही वैलिडेशन और व्यवहार रहते हैं, और दोनों उपयोगकर्ता के मान को URL में कच्चे स्ट्रिंग संयोजन से जोड़ने से बचते हैं।
पहला तरीका स्पष्ट URL-बिल्डिंग प्रिमिटिव्स का उपयोग करता है, जो पाथ सेगमेंट्स को ठीक से एन्कोड और जोड़ते हैं।
import os
import re
import uuid
import validators
import requests
from urllib.parse import urljoin, quote
UUID_V4_PATTERN = r'^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'
def fetch_asset(asset_uuid):
    if not re.match(UUID_V4_PATTERN, asset_uuid):
        raise ValueError('invalid file_id')
    if not validators.uuid(asset_uuid):
        raise ValueError('invalid file_id')
    try:
        normalized_uuid = str(uuid.UUID(asset_uuid))
    except Exception:
        raise ValueError('invalid file_id')
    cfg_host = os.getenv('HOSTNAME', None)
    if cfg_host is None:
        raise ValueError('invalid hostname')
    base_root = f'https://{cfg_host}/v1/files/'
    encoded_uuid = quote(normalized_uuid, safe='')
    target_url = urljoin(base_root, encoded_uuid)
    resp = requests.get(target_url)
    if resp.status_code == 200:
        return resp
दूसरा तरीका requests.Session का सहारा लेता है, जिसमें संयोजित बेस URL और साफ़ पाथ एक्सपैंशन होता है। व्यवहार में, इस विकल्प ने पाइपलाइन की समस्या को सुलझा दिया।
import os
import re
import uuid
import validators
import requests
UUID_V4_PATTERN = r'^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'
def retrieve_file(item_id):
    if not re.match(UUID_V4_PATTERN, item_id):
        raise ValueError('invalid file_id')
    if not validators.uuid(item_id):
        raise ValueError('invalid file_id')
    try:
        clean_id = str(uuid.UUID(item_id))
    except Exception:
        raise ValueError('invalid file_id')
    env_host = os.getenv('HOSTNAME', None)
    if env_host is None:
        raise ValueError('invalid hostname')
    api_base = f'https://{env_host}/v1/files'
    sess = requests.Session()
    resp = sess.get(f"{api_base}/{clean_id}")
    if resp.status_code == 200:
        return resp
क्या बदला और इससे मदद कैसे मिलती है
दोनों रूपांतरों में इनपुट वैलिडेशन ज्यों का त्यों है। फर्क सिर्फ़ इस बात में है कि अंतिम URL कैसे जोड़ा गया है। स्पष्ट URL निर्माण और एन्कोडिंग, या बेस एंडपॉइंट बनाकर Session के ज़रिए रिक्वेस्ट करना, उस डायरेक्ट टेंट फ्लो को तोड़ देता है जिसे स्टैटिक एनालिसिस टूल्स तलाशते हैं। नतीजा वही आउटगोइंग अनुरोध है, बस उसे ऐसे तरीके से बनाया गया है जो फ़्लैग्ड पैटर्न से बचाता है।
क्यों ध्यान देना ज़रूरी है
SSRF, SAST में हाई-सिग्नल श्रेणी है और केवल आंतरिक ट्रैफ़िक के लिए भी रिलीज़ रोक सकती है। URLs बनाने का तरीका समायोजित करके आप पाइपलाइनों को हरा रख सकते हैं, बिना जांच ढीली किए। इससे कोड का उद्देश्य भी स्पष्ट होता है: पाथ सेगमेंट को डेटा की तरह लिया जाता है, न कि फ्री-फ़ॉर्म URL की तरह।
मुख्य बातें
पहचानकर्ता को जितनी सख़्ती चाहिए उतनी वैलिडेट करें, फिर कच्चे स्ट्रिंग इंटरपोलेशन के बजाय सुरक्षित प्रिमिटिव्स या Session-आधारित फ्लो से रिक्वेस्ट URI बनाएँ। इससे कार्यक्षमता बनी रहती है, स्टैटिक एनालिसिस में दिखने वाला SSRF जोखिम घटता है, और आपका CI/CD झूठे-पॉज़िटिव टेंट पैटर्न पर नहीं अटकता।
यह लेख StackOverflow के एक प्रश्न (लेखक: hivegu) और Mahrez BenHamad के उत्तर पर आधारित है।