عنوان:

‫فلسفه وجودی Path.Combine


نویسنده: وحید نصیری
تاریخ: ۱۳۹۰/۰۳/۱۳ ۱۸:۲۹:۰۰
آدرس: www.dntips.ir

عموما اکثر کدهای موجود از روش زیر برای ساخت یک مسیر استفاده می‌کنند:
string path = somePath + "\\" + filename;

اما اگر همین برنامه تحت Mono در لینوکس اجرا شود به مشکل بر می‌خورد زیرا در لینوکس مسیرها این‌بار به صورت زیر هستند:
/somepath/filename

به همین جهت توصیه شده است برای ساخت مسیرها در برنامه‌ی خود، از متد Path.Combine موجود در فضای نام System.IO استفاده کنید زیرا این متد از مقادیر Path.DirectorySeperatorChar و Path.VolumeSeparatorChar جهت تهیه مسیر نهایی استفاده می‌کند. این مقادیر در ویندوز (\) و لینوکس (/) متفاوت بوده و به صورت خودکار در زمان اجرا توسط فریم ورک مورد استفاده مدیریت خواهند شد.
همچنین مزیت دیگر استفاده از Path.Combine ، تعیین اعتبار ورودی است؛ به این معنا که اگر از کاراکترهای غیرمجاز استفاده شود، یک استثناء صادر خواهد شد.

یک مورد دیگر هم شاید بد نباشد همینجا اضافه شود و آن هم فلسفه وجودی Environment.NewLine است. مطابق معمول رسم بر این است که سطر جدید با n\ در انتهای یک رشته مشخص شود اما این همیشه صحیح نیست و در پلتفرم‌های مختلف متفاوت است. Environment.NewLine در ویندوز مساوی r\n\ است و در سیستم‌های مبتنی بر Unix مساوی n\ خواهد بود. به همین جهت بهتر است از این پس بجای n\ از Environment.NewLine جهت مشخص سازی سطر جدید استفاده کنید.

نظرات

  • وحید نصیری در ۱۴۰۳/۰۶/۱۴ ۰۸:۱۴

    یک نکته‌ی تکمیلی: بین مسیرهای مطلق و نسبی در لینوکس و ویندوز، تفاوت وجود دارد!

    فرض کنید با استفاده از قطعه کد زیر، سعی در تبدیل یک مسیر نسبی را به مسیری مطلق، داریم:

    private string GetAbsoluteApiUrl(string url)
        => Uri.TryCreate(url, UriKind.Absolute, out _) ? url : NavigationManager.ToAbsoluteUri(url).ToString();

    این قطعه کد در ویندوز بدون مشکل کار می‌کند. برای مثال اگر مسیر api/test/ را به آن معرفی کنیم، آن‌را تبدیل به یک مسیر مطلق شروع شده‌ی با آدرس دامنه‌ی سایت می‌کند. اما ... این قطعه کد در لینوکس کار نمی‌کند! چون مسیر api/test/ در لینوکس، یک مسیر مطلق به‌شمار می‌رود! برای رفع این مشکل، قرار است چنین API ای در آینده اضافه شود:

    new Uri("/foo", new UriCreationOptions { AllowImplicitFilePaths = false });

  • وحید نصیری در ۱۴۰۴/۰۹/۱۷ ۱۲:۵۲
    مدیریت مسیرهای فایل در .NET: جلوگیری از خطرات امنیتی با Path.Join

    مدیریت مسیرهای فایل یکی از جنبه‌های اساسی است که اغلب نادیده گرفته می‌شود، اما می‌تواند منبع مهمی از آسیب‌پذیری‌های امنیتی باشد. در فریم‌ورک .NET، متدهایی مانند System.IO.Path.Combine برای ترکیب مسیرها طراحی شده‌اند تا کار توسعه‌دهندگان را ساده‌تر کنند. با این حال، رفتارهای ناخواسته این متدها می‌تواند منجر به مشکلات جدی مانند دسترسی ناخواسته به فایل‌های حساس شود. در این ادامه، به بررسی یک هشدار رایج در ابزارهای اسکن کد می‌پردازیم که توسط ابزارهایی مانند GitHub CodeQL شناسایی می‌شود. این هشدار بر روی استفاده نادرست از Path.Combine تمرکز دارد و پیشنهاد می‌کند از Path.Joinبه عنوان جایگزین امن‌تر استفاده کنیم. اگر شما توسعه‌دهنده‌ای هستید که با .NET کار می‌کنید، درک این مسئله می‌تواند به تقویت امنیت برنامه‌هایتان کمک کند.

    توضیح مسئله
    مسئله اصلی در رفتار متد Path.Combine نهفته است. این متد برای ترکیب چندین رشته (string) به یک مسیر فایل واحد طراحی شده، اما یک ویژگی ناخواسته دارد: اگر یکی از آرگومان‌های بعدی (پس از آرگومان اول) یک مسیر مطلق (absolute path) باشد، آرگومان‌های قبلی را به طور کامل نادیده می‌گیرد و فقط مسیر مطلق را برمی‌گرداند. این رفتار "silent" (بدون هیچ هشداری) اتفاق می‌افتد و می‌تواند منجر به مشکلات جدی شود.
    برای مثال، فرض کنید می‌خواهید یک مسیر امن در دایرکتوری برنامه‌تان بسازید:
    string path = Path.Combine("C:\\MyApp\\Data", "C:\\Windows\\System32\\config");
    نتیجه این کد "C:\\Windows\\System32\\config" خواهد بود، نه چیزی که انتظار داشتید! اینجا، آرگومان دوم یک مسیر مطلق است، بنابراین Path.Combine آرگومان اول را دور می‌ریزد. این مسئله می‌تواند به آسیب‌پذیری‌هایی مانند "path traversal" (عبور از مسیر) منجر شود، جایی که ورودی‌های مخرب (مثل ورودی کاربر) برنامه را وادار به دسترسی به فایل‌های حساس سیستم، مانند فایل‌های پیکربندی یا داده‌های محرمانه، می‌کنند.
    این مشکل به ویژه در برنامه‌هایی که با ورودی‌های خارجی (مانند نام فایل از کاربر) کار می‌کنند، خطرناک است. تصور کنید یک برنامه وب که فایل‌ها را در یک فولدر امن ذخیره می‌کند، اما اگر کاربر یک مسیر مطلق وارد کند، برنامه ممکن است به فایل‌های خارج از محدوده دسترسی پیدا کند. این نوع آسیب‌پذیری‌ها می‌توانند به افشای اطلاعات، تغییر فایل‌ها یا حتی اجرای کد مخرب منجر شوند. ابزارهای اسکن کد مانند CodeQL این الگوها را شناسایی می‌کنند تا توسعه‌دهندگان را از چنین ریسک‌هایی آگاه سازند. این هشدار معمولاً در نسخه‌های .NET Core 3.0، 3.1 و .NET 5 تا 10 ظاهر می‌شود، جایی که مدیریت مسیرها بهبود یافته اما همچنان نیاز به دقت دارد.

    راه‌حل قدیمی: استفاده از Path.Combine و محدودیت‌های آن
    در گذشته (و هنوز در بسیاری از کدهای legacy)، توسعه‌دهندگان از Path.Combine برای ساخت مسیرها استفاده می‌کردند. این متد مزایایی مانند مدیریت خودکار جداکننده‌های مسیر (مانند \ در ویندوز) دارد و کد را تمیز نگه می‌دارد. برای مثال:
    string basePath = "C:\\Users\\Me\\Documents";
    string userInput = "report.txt";
    string fullPath = Path.Combine(basePath, userInput);
    // نتیجه مورد انتظار: "C:\\Users\\Me\\Documents\\report.txt"
    این کار در حالت عادی خوب عمل می‌کند. اما همان‌طور که گفتیم، اگر userInput چیزی مانند "C:\\Secret\\file.txt" باشد، نتیجه "C:\\Secret\\file.txt" می‌شود و برنامه به جای ماندن در مسیر امن، به جای دیگری می‌رود.
    این روش قدیمی، اگرچه ساده است، اما فاقد مکانیسم‌های دفاعی در برابر ورودی‌های مخرب است. در واقع، Path.Combine فرض می‌کند همه آرگومان‌ها نسبی (relative) هستند، و این فرض می‌تواند در محیط‌های واقعی (مانند اپلیکیشن‌های وب یا دسکتاپ با ورودی کاربر) نادرست باشد. علاوه بر این، عدم تولید خطا در موارد نامعتبر، برنامه را به سمت رفتارهای غیرمنتظره سوق می‌دهد که دیباگ آن را دشوار می‌کند.

    راه‌حل جدید: مهاجرت به Path.Join برای امنیت بیشتر
    برای حل این مشکل، مایکروسافت متد Path.Join را معرفی کرده که رفتار امن‌تری دارد. برخلاف Path.Combine، این متد آرگومان‌ها را ساده به هم می‌چسباند و اگر یک مسیر مطلق در وسط باشد، یک مسیر نامعقول (nonsensical) مانند "C:\\a\\C:\\b" تولید می‌کند. این مسیر نامعتبر معمولاً باعث شکست عملیات فایل (مانند باز کردن یا نوشتن) می‌شود، که بهتر از دسترسی ناخواسته است؛ حداقل برنامه crash می‌کند و حمله‌کننده موفق نمی‌شود.
    پیشنهاد می‌شود از ترکیب Path.GetFullPath(Path.Join(...)) استفاده کنید. Path.GetFullPath مسیر را normalize می‌کند و اگر مشکلی وجود داشته باشد، خطا می‌دهد. مثال کد جدید:
    string basePath = "C:\\Users\\Me\\Documents";
    string userInput = "C:\\Windows\\System32\\config"; // ورودی مخرب
    string joinedPath = Path.Join(basePath, userInput); // نتیجه: "C:\\Users\\Me\\Documents\\C:\\Windows\\System32\\config"
    string fullPath = Path.GetFullPath(joinedPath); // این خط مسیر را بررسی و normalize می‌کند
    در این حالت، اگر مسیر نامعتبر باشد، Path.GetFullPath ممکن است exception پرتاب کند، که به شما اجازه می‌دهد آن را مدیریت کنید (مثلاً با یک try-catch).

    نکته‌ی تکمیلی: همیشه ورودی‌های کاربر را validate کنید. برای مثال، از Path.IsPathRooted(userInput) استفاده کنید تا چک کنید آیا ورودی مطلق است یا نه، و اگر بود، آن را رد کنید. همچنین، در برنامه‌های وب، از ویژگی‌هایی مانند Path.Combine با احتیاط در کنار فریم‌ورک‌هایی مثل ASP.NET Core استفاده کنید که مکانیسم‌های امنیتی داخلی برای جلوگیری از path traversal دارند. اگر پروژه‌تان از CI/CD استفاده می‌کند، ادغام ابزارهایی مانند CodeQL می‌تواند این هشدارها را زودتر شناسایی کند و کد را امن‌تر نگه دارد.

    نتیجه‌گیری
    مدیریت مسیرهای فایل در .NET بیش از یک وظیفه فنی ساده است؛ این بخشی از تعهد ما به امنیت برنامه‌هاست. با درک رفتارهای ناخواسته Path.Combineو مهاجرت بهPath.Joinهمراه باPath.GetFullPath, می‌توانید ریسک‌های امنیتی را کاهش دهید و برنامه‌هایی مقاوم‌تر بسازید. به یاد داشته باشید، امنیت همیشه اولویت دارد؛ حتی در جزئیات کوچک مانند ترکیب مسیرها.
    • وحید نصیری در ۱۴۰۴/۰۹/۱۷ ۱۳:۰۱
      یک نکته‌ی تکمیلی: روش ممنوع کردن استفاده از متد Path.Combine در برنامه، با استفاده از BannedApiAnalyzers مایکروسافت

      با استفاده از پکیج NuGet‌ای به نام Microsoft.CodeAnalysis.BannedApiAnalyzers، یک فایل BannedSymbols.txt را ایجاد کنید تا APIهای خاصی (مانند متد Path.Combine که در بحث قبلی‌مان مطرح بود) را در کد پروژه‌تان ممنوع کنید. این آنالیزر بخشی از ابزارهای Roslyn است و در زمان کامپایل، اگر از سمبل‌های ممنوعه‌ای استفاده شود، هشدار یا خطا تولید می‌کند. این کار به توسعه‌دهندگان کمک می‌کند تا از APIهای ناامن یا منسوخ دوری کنند و کد امن‌تری بنویسند.

      چطور این کار را انجام دهیم؟
      - نصب پکیج: ابتدا پکیج Microsoft.CodeAnalysis.BannedApiAnalyzers را از طریق NuGet به پروژه .NET خود اضافه کنید. این پکیج در نسخه‌های .NET Core 3.1 و بالاتر پشتیبانی می‌شود.
      - ایجاد فایل: یک فایل متنی به نام BannedSymbols.txt را در ریشه‌ی پروژه (root folder) ایجاد کنید. این فایل باید به عنوان "AdditionalFile" در پروژه اضافه شود (معمولاً به طور خودکار شناسایی می‌شود، اما می‌توانید در فایل .csproj آن را مشخص کنید):
      <ItemGroup>
          <AdditionalFiles Include="$(MSBuildThisFileDirectory)BannedSymbols.txt" Link="Properties/BannedSymbols.txt"/>
        </ItemGroup>

      - فرمت فایل: هر خط در فایل به صورت زیر است:
      <Kind>:<FullyQualifiedName>;<Message>
      • Kind: نوع سمبل، مثل M برای متد (Method)، P برای پراپرتی (Property)، T برای نوع (Type)، و غیره.
      • FullyQualifiedName: نام کامل سمبل، شامل namespace و پارامترها (اگر لازم باشد).
      • Message: پیامی که در زمان هشدار نمایش داده می‌شود، مثل پیشنهاد جایگزین.
      این فایل می‌تواند چندین خط داشته باشد و کامنت‌ها با # شروع می‌شوند.

      نمونه BannedSymbols.txt برای ممنوع کردن Path.Combine
      با توجه به مسئله قبلی‌مان (که Path.Combine می‌تواند منجر به مشکلات امنیتی شود)، می‌توانید فایلی مثل زیر ایجاد کنید تا استفاده از نسخه‌های خاص Path.Combine را ممنوع کنید و توسعه‌دهندگان را به سمت Path.Join هدایت کنید:
      # BannedSymbols.txt
      # این فایل برای ممنوع کردن APIهای ناامن استفاده می‌شود.
      # منبع: https://github.com/dotnet/roslyn-analyzers/blob/master/src/Microsoft.CodeAnalysis.BannedApiAnalyzers/BannedApiAnalyzers.Help.md
      
      M:System.IO.Path.Combine(System.String,System.String);به جای این متد از Path.Join استفاده کنید تا از مشکلات امنیتی مسیر مطلق جلوگیری شود.
      M:System.IO.Path.Combine(System.String,System.String,System.String);به جای این متد از Path.Join استفاده کنید تا از مشکلات امنیتی مسیر مطلق جلوگیری شود.
      M:System.IO.Path.Combine(System.String,System.String,System.String,System.String);به جای این متد از Path.Join استفاده کنید تا از مشکلات امنیتی مسیر مطلق جلوگیری شود.
      M:System.IO.Path.Combine(System.String[]);به جای این متد از Path.Join استفاده کنید تا از مشکلات امنیتی مسیر مطلق جلوگیری شود.
      
      # اگر می‌خواهید کل کلاس Path را محدود کنید (اختیاری، اما معمولاً بیش از حد است):
      # T:System.IO.Path;از کلاس‌های جایگزین یا متدهای امن‌تر استفاده کنید.
      توضیح بیشتر
      • چرا این نمونه؟ اینجا نسخه‌های overload شده Path.Combine را ممنوع کرده‌ام، چون این متد می‌تواند آرگومان‌های قبلی را در صورت وجود مسیر مطلق نادیده بگیرد. پیام هشدار هم مستقیماً پیشنهاد جایگزین می‌دهد.
      • تنظیم شدت هشدار: در فایل .csproj یا با استفاده از EditorConfig، می‌توانید شدت را به "error" تغییر دهید تا کامپایل شکست بخورد (به جای فقط هشدار).
      • نکته: این آنالیزر نه تنها برای امنیت مفید است، بلکه برای جلوگیری از استفاده از APIهای منسوخ (deprecated) یا غیراستاندارد هم کاربرد دارد. مثلاً می‌توانید DateTime.Now را ممنوع کنید و به DateTime.UtcNow هدایت کنید. همچنین، اگر پروژه بزرگی دارید، فایل را در سطح solution قرار دهید تا برای همه پروژه‌ها اعمال شود.
      • تست آن: بعد از اضافه کردن فایل، پروژه را بیلد کنید. اگر از Path.Combine استفاده شده باشد، در لیست خطاها ظاهر می‌شود با پیام شما.
    • هادی مزارعی در ۱۴۰۴/۰۹/۱۸ ۱۳:۵۶
      شاید سوالم جوابی نداشته باشد؛ ولی چرا با وجود چنین اشکالی در استفاده از Path.Combine مایکروسافت عملکرد متد را اصلاح نکرده و یا یک Overload جدید پیاده‌سازی نکرده اما یک متد جدید با نام Path.Join اضافه کرده؟ آیا Path.Combine با اشکال موجود کارکرد ویژه‌ای هم داشته است؟
      • وحید نصیری در ۱۴۰۴/۰۹/۱۸ ۱۴:۲۰
        این موضوع نشان‌دهنده اهمیت backward compatibility در اکوسیستم .NET است. این تصمیم بر اساس اصول طراحی فریم‌ورک گرفته شده تا کدهای موجود آسیب نبینند. متد Path.Combine از نسخه‌های اولیه .NET (حتی پیش از .NET Core) وجود داشته و هدفش ترکیب چندین رشته به یک مسیر فایل معتبر است. رفتار "اشکال‌دار" که اشاره کردی، یعنی نادیده گرفتن آرگومان‌های قبلی اگر یک آرگومان بعدی مطلق (rooted) باشد، در واقع یک ویژگی عمدی است، نه یک باگ. این طراحی برای سناریوهایی مفید است که می‌خواهید مسیر را "ریست" کنید اگر یک بخش مطلق ظاهر شود. برای مثال:
        • در کدهایی که مسیرها را به صورت پویا می‌سازند، این رفتار کمک می‌کند تا اگر یک ورودی خارجی (مثل تنظیمات کاربر) یک مسیر کامل بدهد، برنامه به طور خودکار به آن سوئیچ کند بدون ایجاد مسیرهای نامعتبر.
        • تصور کنید در یک ابزار خط فرمان، کاربر می‌تواند مسیر نسبی یا مطلق وارد کند؛ Path.Combine این انعطاف را فراهم می‌کند بدون نیاز به چک‌های اضافی.
        اما این ویژگی در سناریوهای امنیتی (مثل ورودی‌های کاربر در اپ‌های وب) می‌تونه خطرناک باشه و منجر به path traversal بشه. با این حال، مایکروسافت نمی‌تونه این رفتار رو تغییر بده چون میلیون‌ها خط کد legacy (از جمله لایبرری‌های third-party) به آن وابسته هستن. تغییر آن می‌تونست باعث شکستن compatibility بشه و مهاجرت به نسخه‌های جدید .NET رو سخت کنه.

        چرا یک Overload جدید برای Path.Combine اضافه نشد؟
        اضافه کردن یک overload جدید (مثل یک نسخه با پارامتر اضافی برای کنترل رفتار) ممکن بود، اما مایکروسافت ترجیح داد یک متد جداگانه معرفی کنه. دلایلش:
        • سادگی و وضوح: یک متد جدید (Path.Join) اجازه می‌ده توسعه‌دهندگان به راحتی انتخاب کنن. اگر overload اضافه می‌شد، کد قدیمی همچنان به رفتار قدیمی وابسته می‌موند و کد جدید ممکن بود گیج‌کننده بشه (مثلاً انتخاب اشتباه overload).
        • جلوگیری از پیچیدگی: overloadها می‌تونن منجر به ابهام در رزولوشن متد بشن، مخصوصاً در سناریوهای generic یا extension methods. یک متد تازه، مرز روشنی بین رفتار قدیمی و جدید ایجاد می‌کنه.
        • تمرکز روی امنیت: Path.Join برای concatenation ساده طراحی شده و رفتار rooting نداره، که دقیقاً برای رفع مشکلات امنیتی Path.Combine مناسبه. این کار توسعه‌دهندگان رو تشویق می‌کنه به سمت گزینه امن‌تر برن بدون اجبار به تغییر کدهای قدیمی.

        آیا Path.Combine هنوز کارکرد ویژه‌ای دارد؟
        بله، این متد همچنان برای سناریوهایی که نیاز به "ریست کردن" مسیر بر اساس اجزای مطلق دارن، مفید باقی مونده. مثلاً:
        • در ابزارهای build یا deployment که مسیرها رو از منابع مختلف ترکیب می‌کنن (مثل environment variables که ممکنه مطلق باشن).
        • در کدهای legacy که انتظار این رفتار رو دارن، مثل ترکیب مسیر پایه اپ با مسیر کاربر-تعریف‌شده.
        • در مقایسه با Path.Join، Combine می‌تونه مسیرها رو بیشتر "normalize" کنه (مثل مدیریت separatorها)، اما تمرکز اصلیش روی rooting است.
        اگر از Path.Combine استفاده می‌کنی، همیشه ورودی‌ها رو validate کن (مثل چک کردن Path.IsPathRooted) تا ریسک‌ها رو کم کنی. در پروژه‌های جدید،Path.Join + Path.GetFullPathپیشنهاد می‌شه برای امنیت بیشتر.