عنوان:

‫تولید هدرهای Content Security Policy توسط ASP.NET Core برای برنامه‌های Angular


نویسنده: وحید نصیری
تاریخ: ۱۳۹۶/۰۴/۲۴ ۱۵:۱۵
آدرس: www.dntips.ir
پیشتر مطلب «افزودن هدرهای Content Security Policy به برنامه‌های ASP.NET» را در این سایت مطالعه کرده‌اید. در اینجا قصد داریم معادل آن‌را برای ASP.NET Core تهیه کرده و همچنین نکات مرتبط با برنامه‌های Angular را نیز در آن لحاظ کنیم.


تهیه میان افزار افزودن هدرهای Content Security Policy

کدهای کامل این میان افزار را در ادامه مشاهده می‌کنید:
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;

namespace AngularTemplateDrivenFormsLab.Utils
{
    public class ContentSecurityPolicyMiddleware
    {
        private readonly RequestDelegate _next;

        public ContentSecurityPolicyMiddleware(RequestDelegate next)
        {
            _next = next;
        }

        public Task Invoke(HttpContext context)
        {
            context.Response.Headers.Add("X-Frame-Options", "SAMEORIGIN");
            context.Response.Headers.Add("X-Xss-Protection", "1; mode=block");
            context.Response.Headers.Add("X-Content-Type-Options", "nosniff");

            string[] csp =
            {
              "default-src 'self'",
              "style-src 'self' 'unsafe-inline'",
              "script-src 'self' 'unsafe-inline' 'unsafe-eval'",
              "font-src 'self'",
              "img-src 'self' data:",
              "connect-src 'self'",
              "media-src 'self'",
              "object-src 'self'",
              "report-uri /api/CspReport/Log" //TODO: Add api/CspReport/Log
            };
            context.Response.Headers.Add("Content-Security-Policy", string.Join("; ", csp));
            return _next(context);
        }
    }

    public static class ContentSecurityPolicyMiddlewareExtensions
    {
        /// <summary>
        /// Make sure you add this code BEFORE app.UseStaticFiles();,
        /// otherwise the headers will not be applied to your static files.
        /// </summary>
        public static IApplicationBuilder UseContentSecurityPolicy(this IApplicationBuilder builder)
        {
            return builder.UseMiddleware<ContentSecurityPolicyMiddleware>();
        }
    }
}
که نحوه‌ی استفاده از آن در کلاس آغازین برنامه به صورت ذیل خواهد بود:
public void Configure(IApplicationBuilder app)
{
   app.UseContentSecurityPolicy();

توضیحات تکمیلی

افزودن X-Frame-Options
 context.Response.Headers.Add("X-Frame-Options", "SAMEORIGIN");
از هدر X-FRAME-OPTIONS، جهت منع نمایش و رندر سایت جاری، در iframeهای سایت‌های دیگر استفاده می‌شود. ذکر مقدار SAMEORIGIN آن، به معنای مجاز تلقی کردن دومین جاری برنامه است.


افزودن X-Xss-Protection
 context.Response.Headers.Add("X-Xss-Protection", "1; mode=block");
تقریبا تمام مرورگرهای امروزی قابلیت تشخیص حملات XSS را توسط static analysis توکار خود دارند. این هدر، آنالیز اجباری XSS را فعال کرده و همچنین تنظیم حالت آن به block، نمایش و رندر قسمت مشکل‌دار را به طور کامل غیرفعال می‌کند.


افزودن X-Content-Type-Options
 context.Response.Headers.Add("X-Content-Type-Options", "nosniff");
وجود این هدر سبب می‌شود تا مرورگر، حدس‌زدن نوع فایل‌ها، درخواست‌ها و محتوا را کنار گذاشته و صرفا به content-type ارسالی توسط سرور اکتفا کند. به این ترتیب برای مثال امکان لینک کردن یک فایل غیرجاوا اسکریپتی و اجرای آن به صورت کدهای جاوا اسکریپت، چون توسط تگ script ذکر شده‌است، غیرفعال می‌شود. در غیراینصورت مرورگر هرچیزی را که توسط تگ script به صفحه لینک شده باشد، صرف نظر از content-type واقعی آن، اجرا خواهد کرد.


افزودن Content-Security-Policy
string[] csp =
            {
              "default-src 'self'",
              "style-src 'self' 'unsafe-inline'",
              "script-src 'self' 'unsafe-inline' 'unsafe-eval'",
              "font-src 'self'",
              "img-src 'self' data:",
              "connect-src 'self'",
              "media-src 'self'",
              "object-src 'self'",
              "report-uri /api/CspReport/Log" //TODO: Add api/CspReport/Log
            };
context.Response.Headers.Add("Content-Security-Policy", string.Join("; ", csp));
وجود این هدر، تزریق کدها و منابع را از دومین‌های دیگر غیرممکن می‌کند. برای مثال ذکر self در اینجا به معنای مجاز بودن الصاق و اجرای اسکریپت‌ها، شیوه‌نامه‌ها، تصاویر و اشیاء، صرفا از طریق دومین جاری برنامه است و هرگونه منبعی که از دومین‌های دیگر به برنامه تزریق شود، قابلیت اجرایی و یا نمایشی نخواهد داشت.

در اینجا ذکر unsafe-inline و unsafe-eval را مشاهده می‌کنید. برنامه‌های Angular به همراه شیوه‌نامه‌های inline و یا بکارگیری متد eval در مواردی خاص هستند. اگر این دو گزینه ذکر و فعال نشوند، در کنسول developer مرورگر، خطای بلاک شدن آن‌ها را مشاهده کرده و همچنین برنامه از کار خواهد افتاد.

یک نکته: با فعالسازی گزینه‌ی aot-- در حین ساخت برنامه، می‌توان unsafe-eval را نیز حذف کرد.


استفاده از فایل web.config برای تعریف SameSite Cookies

یکی از پیشنهادهای اخیر ارائه شده‌ی جهت مقابله‌ی با حملات CSRF و XSRF، قابلیتی است به نام  Same-Site Cookies. به این ترتیب مرورگر، کوکی سایت جاری را به همراه یک درخواست ارسال آن به سایت دیگر، پیوست نمی‌کند (کاری که هم اکنون با درخواست‌های Cross-Site صورت می‌گیرد). برای رفع این مشکل، با این پیشنهاد امنیتی جدید، تنها کافی است SameSite، به انتهای کوکی اضافه شود:
 Set-Cookie: sess=abc123; path=/; SameSite

نگارش‌های بعدی ASP.NET Core، ویژگی SameSite را نیز به عنوان CookieOptions لحاظ کرده‌اند. همچنین یک سری از کوکی‌های خودکار تولیدی توسط آن مانند کوکی‌های anti-forgery به صورت خودکار با این ویژگی تولید می‌شوند.
اما مدیریت این مورد برای اعمال سراسری آن، با کدنویسی میسر نیست (مگر اینکه مانند نگارش‌های بعدی ASP.NET Core پشتیبانی توکاری از آن صورت گیرد). به همین جهت می‌توان از ماژول URL rewrite مربوط به IIS برای افزودن ویژگی SameSite به تمام کوکی‌های تولید شده‌ی توسط سایت، کمک گرفت. برای این منظور تنها کافی است فایل web.config را ویرایش کرده و موارد ذیل را به آن اضافه کنید:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.webServer>
    <rewrite>
      <outboundRules>
        <clear />
        <!-- https://scotthelme.co.uk/csrf-is-dead/ -->
        <rule name="Add SameSite" preCondition="No SameSite">
          <match serverVariable="RESPONSE_Set_Cookie" pattern=".*" negate="false" />
          <action type="Rewrite" value="{R:0}; SameSite=lax" />
          <conditions></conditions>
        </rule>
        <preConditions>
          <preCondition name="No SameSite">
            <add input="{RESPONSE_Set_Cookie}" pattern="." />
            <add input="{RESPONSE_Set_Cookie}" pattern="; SameSite=lax" negate="true" />
          </preCondition>
        </preConditions>
      </outboundRules>
    </rewrite>
  </system.webServer>
</configuration>


لاگ کردن منابع بلاک شده‌ی توسط مرورگر در سمت سرور

اگر به هدر Content-Security-Policy دقت کنید، گزینه‌ی آخر آن، ذکر اکشن متدی در سمت سرور است:
   "report-uri /api/CspReport/Log" //TODO: Add api/CspReport/Log
با تنظیم این مورد، می‌توان موارد بلاک شده را در سمت سرور لاگ کرد. اما این اطلاعات ارسالی به سمت سرور، فرمت خاصی را دارند:
{
  "csp-report": {
    "document-uri": "http://localhost:5000/untypedSha",
    "referrer": "",
    "violated-directive": "script-src",
    "effective-directive": "script-src",
    "original-policy": "default-src 'self'; style-src 'self'; script-src 'self'; font-src 'self'; img-src 'self' data:; connect-src 'self'; media-src 'self'; object-src 'self'; report-uri /api/Home/CspReport",
    "disposition": "enforce",
    "blocked-uri": "eval",
    "line-number": 21,
    "column-number": 8,
    "source-file": "http://localhost:5000/scripts.bundle.js",
    "status-code": 200,
    "script-sample": ""
  }
}
به همین جهت ابتدا نیاز است توسط JsonProperty کتابخانه‌ی JSON.NET، معادل این خواص را تولید کرد:
    class CspPost
    {
        [JsonProperty("csp-report")]
        public CspReport CspReport { get; set; }
    }

    class CspReport
    {
        [JsonProperty("document-uri")]
        public string DocumentUri { get; set; }

        [JsonProperty("referrer")]
        public string Referrer { get; set; }

        [JsonProperty("violated-directive")]
        public string ViolatedDirective { get; set; }

        [JsonProperty("effective-directive")]
        public string EffectiveDirective { get; set; }

        [JsonProperty("original-policy")]
        public string OriginalPolicy { get; set; }

        [JsonProperty("disposition")]
        public string Disposition { get; set; }

        [JsonProperty("blocked-uri")]
        public string BlockedUri { get; set; }

        [JsonProperty("line-number")]
        public int LineNumber { get; set; }

        [JsonProperty("column-number")]
        public int ColumnNumber { get; set; }

        [JsonProperty("source-file")]
        public string SourceFile { get; set; }

        [JsonProperty("status-code")]
        public string StatusCode { get; set; }

        [JsonProperty("script-sample")]
        public string ScriptSample { get; set; }
    }
اکنون می‌توان بدنه‌ی درخواست را استخراج و سپس به این شیء ویژه نگاشت کرد:
namespace AngularTemplateDrivenFormsLab.Controllers
{
    [Route("api/[controller]")]
    public class CspReportController : Controller
    {
        [HttpPost("[action]")]
        [IgnoreAntiforgeryToken]
        public async Task<IActionResult> Log()
        {
            CspPost cspPost;
            using (var bodyReader = new StreamReader(this.HttpContext.Request.Body))
            {
                var body = await bodyReader.ReadToEndAsync().ConfigureAwait(false);
                this.HttpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(body));
                cspPost = JsonConvert.DeserializeObject<CspPost>(body);
            }

            //TODO: log cspPost

            return Ok();
        }
    }
}
در اینجا نحوه‌ی استخراج Request.Body را به صورت خام را مشاهده می‌کنید. سپس توسط متد DeserializeObject کتابخانه‌ی JSON.NET، این رشته به شیء CspPost نگاشت شده‌است.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.

نظرات

  • وحید نصیری در ۱۳۹۶/۱۱/۲۰ ۱۴:۹
    یک نکته‌ی تکمیلی
    - کتابخانه‌ای غنی برای تولید این هدرها: NWebsec  
    - مثالی از آن
    • وحید نصیری در ۱۳۹۶/۱۲/۱۰ ۱۰:۴۱
      یک نکته‌ی تکمیلی  
      کتابخانه‌ای دیگر برای تولید هدرهای امنیتی: OwaspHeaders.Core  
  • وحید نصیری در ۱۳۹۶/۱۲/۲۲ ۱۱:۴۴
    یک نکته‌ی تکمیلی: پردازش صحیح X-Content-Type-Options در کروم 65

    اگر هدر X-Content-Type-Options را به nosniff تنظیم کرده باشید، کروم 65 از اجرای فایل‌های اسکریپت با خطای زیر سر باز می‌زند:
    Refused to execute script from '<URL>' because its MIME type ('') is not executable, and strict MIME type checking is enabled.
    چون تنظیم پیش فرض app.UseStaticFiles، هیچ نوع content-type ایی را نمی‌شناسد.
    برای رفع آن باید به صورت زیر content-type صحیحی را به فایل‌های اسکریپت تولیدی نسبت داد:
    var provider = new FileExtensionContentTypeProvider();
    app.UseStaticFiles(new StaticFileOptions
    {
        ContentTypeProvider = provider
    });
  • رضا سلیم در ۱۳۹۸/۰۱/۰۳ ۳:۱۳
    بعد از افزودن این هدرهای امنیتی خطای زیر رو دریافت میکنم و تصاویری که از خارج از پروژه باشه نمیتونه بارگذاری کنه
    Content Security Policy: The page’s settings blocked the loading of a resource at https://mdbootstrap.com/img/Photos/Others/sidenav3.jpg (“img-src”).
    • وحید نصیری در ۱۳۹۸/۰۱/۰۳ ۷:۱۷
      بله. تمام هدف 'img-src 'self همین هست. اگر فکر می‌کنید دومینی امن است آن‌را به صورت http://site.com در انتهای لیست img-src اضافه کنید.
  • وحید نصیری در ۱۳۹۸/۰۷/۰۹ ۱۲:۷
    «خداحافظی با حملات CSRF و cross-site leak برای همیشه از کروم 80 به بعد» با اعمال SameSite Cookies بحث شده‌ی در این مطلب به صورت پیش‌فرض. 
  • سید سجاد هادیان امرئی در ۱۳۹۸/۰۷/۱۵ ۲۳:۹
    با سلام؛ آیا افزودن هدر‌های فوق باعث بلاک شدن iframe ‌های سایت‌های دیگر مانند GoogleMap در سایت ما خواهد شد؟ اگر اینطوری است برای اجازه دادن به جلوگیری از این بلاک شدن، باید چه تنظیماتی را به هدر‌های فوق افزود؟
    • وحید نصیری در ۱۳۹۸/۰۷/۱۵ ۲۳:۳۲
      In Firefox and IE
      X-Frame-Options: allow-from https://example.com/
      
      In Chrome and Safari: 
      Content-Security-Policy: frame-ancestors domain.com
      • سید سجاد هادیان امرئی در ۱۳۹۸/۰۷/۲۱ ۱:۲۹
        با افزودن تنظیمات فوق نتیجه ای حاصل نشد اما با افزودن تنظیمات "child-src *.google.com" نمایش iframe نقشه گوگل ممکن شد.
        • بهمن خلفی در ۱۳۹۹/۰۶/۱۷ ۱۶:۵۷
          سلام ، میشه این کدی که به برنامه اضافه کردید تا نقشه نمایش بده رو اینجا بذارید؟
  • سید سجاد هادیان امرئی در ۱۳۹۸/۰۸/۱۳ ۹:۵۶
     با سلام؛ برای مجاز دانستن دریافت ورودی‌هایی که قبلا توسط Allow Html  و یا ValidationInput[false] فعال می‌شد، چه تنظیماتی رو باید به هدر‌های فوق افزود؟ با تشکر
  • نقوی در ۱۳۹۹/۰۶/۲۴ ۲۰:۱۱

    سلام؛ برای نمایش آیکن مربوط به اینماد، کد مربوطه رو به صورت زیر تغییر دادم:
    "img-src 'self' data: blob: https://trustseal.enamad.ir",
    با یک مسئله رو به رو شدم اینکه اینماد میخواد یک کوکی رو تو سایت ذخیره کنه. هشدار مربوطه به شکل فوق هستش
    ممنون میشم راهنمایی بفرمایید که مشکل کجاست. آیا باید از ذخیره کوکی ارسال شده جلوگیری کنم؟
  • میلاد احمدی مقدم در ۱۴۰۰/۰۷/۳۰ ۴:۷
    خیلی ممنون از پکیج DNT Common.
    به نظر شما مشکلی نیست که url ـی که لاگ‌های csp داخلش ثبت (api/CspReportController/Log ) میشه پابلیکه و همه اون میدون، خطری ایجاد نمیکنه مثل اینکه کسی بیاد داده جعلی وارد کنه یا روش spam بزنن ؟
    • وحید نصیری در ۱۴۰۰/۰۸/۲۴ ۱۳:۳۰
      خطری ایجاد نمیکنه 
      این اطلاعات باید به صورت عمومی در هدر درخواست به مرورگر ارسال شود تا آن‌را تشخیص داده و استفاده کند. نحوه‌ی کارکرد استاندارش به همین صورت است. از هر روش دیگری هم استفاده کنید به همین صورت باید باشد.
  • وحید نصیری در ۱۴۰۰/۰۹/۰۹ ۱۰:۴۰
    یک نکته‌ی تکمیلی: دات نت 6 و hot reload آن

    اگر برنامه‌های مبتنی بر دات نت 6 را به همراه فعال بودن Content Security Policy اجرا کنید، با خطای زیر در مرورگر مواجه خواهید شد:
    Refused to connect to ws: because it violates the Content Security Policy directive
    {
       "csp-report":{
          "document-uri":"http://localhost:5051/",
          "referrer":"",
          "violated-directive":"connect-src",
          "effective-directive":"connect-src",
          "original-policy":"default-src 'self' blob:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' 'unsafe-eval' ; font-src 'self'; img-src 'self' data: blob:; connect-src 'self'; media-src 'self'; object-src 'self' blob:; report-uri /api/CspReport/Log",
          "disposition":"enforce",
          "blocked-uri":"wss://localhost:52837",
          "line-number":234,
          "column-number":25,
          "source-file":"http://localhost:5051/_framework/aspnetcore-browser-refresh.js",
          "status-code":200,
          "script-sample":""
       }
    }
    برای رفع آن فقط کافی است تنظیم زیر را اضافه/ویرایش کنید:
    connect-src 'self' wss://localhost:*