Home » Создайте свои собственные заголовки безопасности

Создайте свои собственные заголовки безопасности

Правильные заголовки безопасности являются обязательными для вашего веб-сайта, управляемого Optimizely. Существует множество инструментов, которые помогут в этом, но, когда это возможно, я предпочитаю использовать собственное решение, особенно для такой низкоуровневой функции, как заголовки безопасности. Нет необходимости поддерживать пакет NuGet в актуальном состоянии. Не нужно беспокоиться о постоянных обновлениях версий. Все это содержится в базе кода, которой я управляю. Я сделал это с помощью специального промежуточного программного обеспечения, которое можно настроить в Startup.cs. Заголовки, которые можно добавить, включают:

  • Параметры X-Content-Type
  • Параметры X-Frame
  • Сервер
  • Строгая транспортная безопасность
  • X-XSS-Защита
  • Политика рефереров
  • Политика разрешений
  • Политика безопасности контента
  • X-Параметры загрузки

Первое, что мне нужно сделать, это создать несколько классов констант, которые будут содержать все имена и значения заголовков, которые мне понадобятся. Каждый класс содержит строку имени заголовка и строки любых параметров. Политика безопасности контента и политика разрешений, будучи немного более сложными, также нуждаются в перечислении, чтобы охватить некоторые параметры.

Параметры X-Content-Type

/// 
/// X-Content-Type-Options-related constants.
/// 
public static class ContentTypeOptionsConstants
{
    /// 
    /// Header value for X-Content-Type-Options
    /// 
    public static readonly string Header = "X-Content-Type-Options";

    /// 
    /// Disables content sniffing
    /// 
    public static readonly string NoSniff = "nosniff";
}

В данном случае у меня есть только опции nosniff, потому что именно их я буду использовать.

Параметры X-Frame

/// 
/// X-Frame-Options-related constants.
/// 
public static class FrameOptionsConstants
{
    /// 
    /// The header value for X-Frame-Options
    /// 
    public static readonly string Header = "X-Frame-Options";

    /// 
    /// The page cannot be displayed in a frame, regardless of the site attempting to do so.
    /// 
    public static readonly string Deny = "DENY";

    /// 
    /// The page can only be displayed in a frame on the same origin as the page itself.
    /// 
    public static readonly string SameOrigin = "SAMEORIGIN";

    /// 
    /// The page can only be displayed in a frame on the specified origin. {0} specifies the format string
    /// 
    public static readonly string AllowFromUri = "ALLOW-FROM {0}";
}

Сервер

/// 
/// Server headery-related constants.
/// 
public static class ServerConstants
{
    /// 
    /// The header value for X-Powered-By
    /// 
    public static readonly string Header = "Server";
}

Строгая транспортная безопасность

/// 
/// Strict-Transport-Security-related constants.
/// 
public static class StrictTransportSecurityConstants
{
    /// 
    /// Header value for Strict-Transport-Security
    /// 
    public static readonly string Header = "Strict-Transport-Security";

    /// 
    /// Tells the user-agent to cache the domain in the STS list for the provided number of seconds {0} 
    /// 
    public static readonly string MaxAge = "max-age={0}";

    /// 
    /// Tells the user-agent to cache the domain in the STS list for the provided number of seconds {0} and include any subdomains.
    /// 
    public static readonly string MaxAgeIncludeSubdomains = "max-age={0}; includeSubDomains";

    /// 
    /// Tells the user-agent to remove, or not cache the host in the STS cache.
    /// 
    public static readonly string NoCache = "max-age=0";
}

X-XSS-Защита

/// 
/// X-XSS-Protection-related constants.
/// 
public static class XssProtectionConstants
{
    /// 
    /// Header value for X-XSS-Protection
    /// 
    public static readonly string Header = "X-XSS-Protection";

    /// 
    /// Enables the XSS Protections
    /// 
    public static readonly string Enabled = "1";

    /// 
    /// Disables the XSS Protections offered by the user-agent.
    /// 
    public static readonly string Disabled = "0";

    /// 
    /// Enables XSS protections and instructs the user-agent to block the response in the event that script has been inserted from user input, instead of sanitizing.
    /// 
    public static readonly string Block = "1; mode=block";

    /// 
    /// A partially supported directive that tells the user-agent to report potential XSS attacks to a single URL. Data will be POST'd to the report URL in JSON format. 
    /// {0} specifies the report url, including protocol
    /// 
    public static readonly string Report = "1; report={0}";
}

Политика рефереров

/// 
/// Referrer Policy related constants
/// 
public static class ReferrerPolicyConstants
{
    /// 
    /// Header value for Referrer-Policy
    /// 
    public static readonly string Header = "Referrer-Policy";

    public static readonly string NoReferrer = "no-referrer";

    public static readonly string NoReferrerWhenDowngrade = "no-referrer-when-downgrade";

    public static readonly string SameOrigin = "same-origin";

    public static readonly string Origin = "origin";

    public static readonly string StrictOrigin = "strict-origin";

    public static readonly string OriginWhenCrossOrigin = "origin-when-cross-origin";

    public static readonly string StrictOriginWhenCrossOrigin = "strict-origin-when-cross-origin";

    public static readonly string UnsafeUrl = "unsafe-url";
}

Политика разрешений

/// 
/// Permissions Policy related constants
/// 
public static class PermissionsPolicyConstants
{
    /// 
    /// Header value for Permissions-Policy
    /// 
    public static readonly string Header = "Permissions-Policy";
}

При настройке политики разрешений вы можете установить разрешения для различных функций. Различные функции, которые я поместил в перечисление:

/// 
/// Enum of some permission policies
/// 
public enum PermissionPolicy
{
    [EnumSelectionDescription(Text = "Accelerometer", Value = "accelerometer")]
    Accelerometer = 1,

    [EnumSelectionDescription(Text = "Camera", Value = "camera")]
    Camera = 2,

    [EnumSelectionDescription(Text = "Geolocation", Value = "")]
    Geolocation = 3,

    [EnumSelectionDescription(Text = "Gyroscope", Value = "gyroscope")]
    Gyroscope = 4,

    [EnumSelectionDescription(Text = "Magnetometer", Value = "magnetometer")]
    Magnetometer = 5,

    [EnumSelectionDescription(Text = "Microphone", Value = "microphone")]
    Microphone = 6,

    [EnumSelectionDescription(Text = "Payment", Value = "payment")]
    Payment = 7,

    [EnumSelectionDescription(Text = "Usb", Value = "usb")]
    Usb = 8
}

Политика безопасности контента

/// 
/// Permissions Policy related constants
/// 
public static class ContentSecurityPolicyConstants
{
    /// 
    /// Header value for Permissions-Policy
    /// 
    public static readonly string Header = "Content-Security-Policy";

}

Политику безопасности контента необходимо настроить для различных типов контента. Я также управляю ими с помощью перечисления:

public enum ContentSecurityPolicy
{
    [EnumSelectionDescription(Text = "DefaultSource", Value = "default-src 'self'")]
    DefaultSource = 1,

    [EnumSelectionDescription(Text = "ConnectSource", Value = " connect-src * 'self' data: https:")]
    ConnectSource = 2,

    [EnumSelectionDescription(Text = "FontSource", Value = " font-src 'self' data: https:")]
    FontSource = 3,

    [EnumSelectionDescription(Text = "FrameSource", Value = " frame-src 'self' data: https:")]
    FrameSource = 4,

    [EnumSelectionDescription(Text = "ImageSource", Value = "  img-src * 'self' data: https: blob:")]
    ImageSource = 5,

    [EnumSelectionDescription(Text = "ScriptSource", Value = " script-src 'self' 'nonce-{nonceValue}' 'strict-dynamic' ")]
    ScriptSource = 6,

    [EnumSelectionDescription(Text = "StyleSource", Value = " style-src 'self' 'unsafe-inline' *")]
    StyleSource = 7,

    [EnumSelectionDescription(Text = "FormAction", Value = " form-action 'self' data: https:")]
    FormAction = 8,

    [EnumSelectionDescription(Text = "MediaSource", Value = " media-src 'self' data: https: blob:")]
    MediaSource = 9
}

X-Параметры загрузки

/// 
/// Permissions Policy related constants
/// 
public static class XDownloadOptionsConstants
{
    /// 
    /// Header value for Permissions-Policy
    /// 
    public static readonly string Header = "X-Download-Options";

    public static readonly string NoOpen = "noopen";
}

Настоящая работа, которую я делаю, — это класс-строитель SecurityHeadersBuilder. Этот конструктор имеет множество методов для установки различных значений заголовков. Затем я создаю метод, который создает политику безопасности. Концептуально у вас может быть множество политик. Мне нужен только один, поэтому у меня есть метод по умолчанию, который создает все мои заголовки. Он сохраняет их в классе политики.

Read more:  Выводы НФЛ: Биллс и Стилерс максимально используют свои возможности

Сначала класс политики. Это довольно просто, у него просто есть метод добавления и удаления для установки заголовков.

/// 
/// The security headers policy
/// 
public class SecurityHeadersPolicy
{
    /// 
    /// Headers to add
    /// 
    public IDictionary SetHeaders { get; }
        = new Dictionary();

    /// 
    /// Headers to remove
    /// 
    public ISet RemoveHeaders { get; }
        = new HashSet();
}

Он сохраняет эти значения в словаре.

Затем конструктор добавляет заголовки в эту политику:

/// 
/// Middle ware to create the desired security headers
/// 
public class SecurityHeadersBuilder
{
    private readonly SecurityHeadersPolicy _policy = new();

    /// 
    /// The number of seconds in one year
    /// 
    public const int OneYearInSeconds = 60 * 60 * 24 * 365;

    private CompositeFormat FrameOptionsAllowFromUri { get; set; } = CompositeFormat.Parse(FrameOptionsConstants.AllowFromUri);

    private CompositeFormat XssProtectionConstantsReport { get; set; } = CompositeFormat.Parse(XssProtectionConstants.Report);

    private CompositeFormat StrictTransportSecurityConstantsMaxAge { get; set; } = CompositeFormat.Parse(StrictTransportSecurityConstants.MaxAge);

    private CompositeFormat StrictTransportSecurityConstantsMaxAgeIncludeSubdomains { get; set; } = CompositeFormat.Parse(StrictTransportSecurityConstants.MaxAgeIncludeSubdomains);

    /// 
    /// Add default headers in accordance with most secure approach
    /// 
    public SecurityHeadersBuilder AddDefaultSecurePolicy()
    {
        this.AddXssProtectionBlock();
        this.AddStrictTransportSecurityMaxAge();
        this.AddPermissionsPolicy();
        this.AddXssProtectionBlock();
        this.AddContentTypeOptionsNoSniff();
        this.AddReferrerPolicyStrictOriginWhenCrossOrigin();
        this.AddXDownloadOptionsNoOpen();

        return this;
    }

    /// 
    /// Add X-Frame-Options DENY to all requests.
    /// The page cannot be displayed in a frame, regardless of the site attempting to do so
    /// 
    public SecurityHeadersBuilder AddFrameOptionsDeny()
    {
        this._policy.SetHeaders[FrameOptionsConstants.Header] = FrameOptionsConstants.Deny;
        return this;
    }

    /// 
    /// Add X-Frame-Options SAMEORIGIN to all requests.
    /// The page can only be displayed in a frame on the same origin as the page itself.
    /// 
    public SecurityHeadersBuilder AddFrameOptionsSameOrigin()
    {
        this._policy.SetHeaders[FrameOptionsConstants.Header] = FrameOptionsConstants.SameOrigin;
        return this;
    }

    /// 
    /// Add X-Frame-Options ALLOW-FROM {uri} to all requests, where the uri is provided
    /// The page can only be displayed in a frame on the specified origin.
    /// 
    /// The uri of the origin in which the page may be displayed in a frame
    public SecurityHeadersBuilder AddFrameOptionsSameOrigin(string uri)
    {
        this._policy.SetHeaders[FrameOptionsConstants.Header] = string.Format(CultureInfo.InvariantCulture, this.FrameOptionsAllowFromUri, uri);
        return this;
    }


    /// 
    /// Add X-XSS-Protection 1 to all requests.
    /// Enables the XSS Protections
    /// 
    public SecurityHeadersBuilder AddXssProtectionEnabled()
    {
        this._policy.SetHeaders[XssProtectionConstants.Header] = XssProtectionConstants.Enabled;
        return this;
    }

    /// 
    /// Add X-XSS-Protection 0 to all requests.
    /// Disables the XSS Protections offered by the user-agent.
    /// 
    public SecurityHeadersBuilder AddXssProtectionDisabled()
    {
        this._policy.SetHeaders[XssProtectionConstants.Header] = XssProtectionConstants.Disabled;
        return this;
    }

    /// 
    /// Add X-XSS-Protection 1; mode=block to all requests.
    /// Enables XSS protections and instructs the user-agent to block the response in the event that script has been inserted from user input, instead of sanitizing.
    /// 
    public SecurityHeadersBuilder AddXssProtectionBlock()
    {
        this._policy.SetHeaders[XssProtectionConstants.Header] = XssProtectionConstants.Block;
        return this;
    }

    /// 
    /// Add X-XSS-Protection 1; report=http://site.com/report to all requests.
    /// A partially supported directive that tells the user-agent to report potential XSS attacks to a single URL. Data will be POST'd to the report URL in JSON format.
    /// 
    public SecurityHeadersBuilder AddXssProtectionReport(string reportUrl)
    {
        this._policy.SetHeaders[XssProtectionConstants.Header] =
            string.Format(CultureInfo.InvariantCulture, this.XssProtectionConstantsReport, reportUrl);
        return this;
    }

    /// 
    /// Add Strict-Transport-Security max-age= to all requests.
    /// Tells the user-agent to cache the domain in the STS list for the number of seconds provided.
    /// 
    public SecurityHeadersBuilder AddStrictTransportSecurityMaxAge(int maxAge = OneYearInSeconds)
    {
        this._policy.SetHeaders[StrictTransportSecurityConstants.Header] =
            string.Format(CultureInfo.InvariantCulture, this.StrictTransportSecurityConstantsMaxAge, maxAge);
        return this;
    }

    /// 
    /// Add Strict-Transport-Security max-age=; includeSubDomains to all requests.
    /// Tells the user-agent to cache the domain in the STS list for the number of seconds provided and include any subdomains.
    /// 
    public SecurityHeadersBuilder AddStrictTransportSecurityMaxAgeIncludeSubDomains(int maxAge = OneYearInSeconds)
    {
        this._policy.SetHeaders[StrictTransportSecurityConstants.Header] =
            string.Format(CultureInfo.InvariantCulture, this.StrictTransportSecurityConstantsMaxAgeIncludeSubdomains, maxAge);
        return this;
    }

    /// 
    /// Add Strict-Transport-Security max-age=0 to all requests.
    /// Tells the user-agent to remove, or not cache the host in the STS cache
    /// 
    public SecurityHeadersBuilder AddStrictTransportSecurityNoCache()
    {
        this._policy.SetHeaders[StrictTransportSecurityConstants.Header] =
            StrictTransportSecurityConstants.NoCache;
        return this;
    }

    /// 
    /// Add X-Content-Type-Options nosniff to all requests.
    /// Can be set to protect against MIME type confusion attacks.
    /// 
    public SecurityHeadersBuilder AddContentTypeOptionsNoSniff()
    {
        this._policy.SetHeaders[ContentTypeOptionsConstants.Header] = ContentTypeOptionsConstants.NoSniff;
        return this;
    }

    /// 
    /// Removes the Server header from all responses
    /// 
    public SecurityHeadersBuilder RemoveServerHeader()
    {
        this._policy.RemoveHeaders.Add(ServerConstants.Header);
        return this;
    }

    /// 
    /// Add the XDownload option headers
    /// 
    /// 
    public SecurityHeadersBuilder AddXDownloadOptionsNoOpen()
    {
        this._policy.SetHeaders[XDownloadOptionsConstants.Header] = XDownloadOptionsConstants.NoOpen;
        return this;

    }

    /// 
    /// Adds a custom header to all requests
    /// 
    /// The header name
    /// The value for the header
    /// 
    public SecurityHeadersBuilder AddCustomHeader(string header, string value)
    {
        if (string.IsNullOrEmpty(header))
        {
            throw new ArgumentNullException(nameof(header));
        }

        this._policy.SetHeaders[header] = value;
        return this;
    }

    /// 
    /// Remove a header from all requests
    /// 
    /// The to remove
    /// 
    public SecurityHeadersBuilder RemoveHeader(string header)
    {
        if (string.IsNullOrEmpty(header))
        {
            throw new ArgumentNullException(nameof(header));
        }

        this._policy.RemoveHeaders.Add(header);
        return this;
    }

    /// 
    /// Add referrer policy header of NoReferrer
    /// 
    /// 
    public SecurityHeadersBuilder AddReferrerPolicyNoReferrer()
    {
        this._policy.SetHeaders[ReferrerPolicyConstants.Header] = ReferrerPolicyConstants.NoReferrer;
        return this;
    }

    /// 
    /// Add referrer policy header of NoReferrer
    /// 
    /// 
    public SecurityHeadersBuilder AddReferrerPolicyStrictOriginWhenCrossOrigin()
    {
        this._policy.SetHeaders[ReferrerPolicyConstants.Header] = ReferrerPolicyConstants.StrictOriginWhenCrossOrigin;
        return this;
    }

    /// 
    /// Add the permissions policy
    /// 
    /// 
    public SecurityHeadersBuilder AddPermissionsPolicy()
    {
        // Get all permissions policies
        var permissionPolicies = Enum.GetValues();

        // Loop through the policies
        var policy = (from permissionPolicy in permissionPolicies
                      select permissionPolicy.Value() into value
                      where !string.IsNullOrEmpty(value)
                      select $"{value}=()").ToList();

        this._policy.SetHeaders[PermissionsPolicyConstants.Header] = string.Join(',', policy.ToArray());
        return this;
    }

    /// 
    /// Builds a new  using the entries added.
    /// 
    /// The constructed .
    public SecurityHeadersPolicy Build() => this._policy;
}

Разбираем, что здесь происходит:

Сначала я создаю политику:

private readonly SecurityHeadersPolicy _policy = new();

Затем я создаю константу для хранения 1 года в секундах (для заголовка Stricct Transport Security я устанавливаю максимальный возраст, который должен составлять год).

/// 
/// The number of seconds in one year
/// 
public const int OneYearInSeconds = 60 * 60 * 24 * 365;

Далее у меня есть несколько значений заголовков, которые требуют замены строк. Для каждого из них я создаю объект CompositeFormat, анализируя значение заголовка. Создание CompositeFormat кэширует анализ этой строки, добавляя небольшой прирост производительности при последующих вызовах. Замена произойдет позже:

private CompositeFormat FrameOptionsAllowFromUri { get; set; } = CompositeFormat.Parse(FrameOptionsConstants.AllowFromUri);

private CompositeFormat XssProtectionConstantsReport { get; set; } = CompositeFormat.Parse(XssProtectionConstants.Report);

private CompositeFormat StrictTransportSecurityConstantsMaxAge { get; set; } = CompositeFormat.Parse(StrictTransportSecurityConstants.MaxAge);

private CompositeFormat StrictTransportSecurityConstantsMaxAgeIncludeSubdomains { get; set; } = CompositeFormat.Parse(StrictTransportSecurityConstants.MaxAgeIncludeSubdomains);

В этом случае, если вы посмотрите выше, вы увидите, что FrameOptionsConstants.AllowFromUri равно «ALLOW-FROM {0}». Когда я устанавливаю этот заголовок, я заменю «{0}» URL-адресом.

Затем я создаю метод для установки каждого значения заголовка. Например, для FrameOptions у меня есть три метода установки разных значений:

/// 
/// Add X-Frame-Options DENY to all requests.
/// The page cannot be displayed in a frame, regardless of the site attempting to do so
/// 
public SecurityHeadersBuilder AddFrameOptionsDeny()
{
    this._policy.SetHeaders[FrameOptionsConstants.Header] = FrameOptionsConstants.Deny;
    return this;
}

/// 
/// Add X-Frame-Options SAMEORIGIN to all requests.
/// The page can only be displayed in a frame on the same origin as the page itself.
/// 
public SecurityHeadersBuilder AddFrameOptionsSameOrigin()
{
    this._policy.SetHeaders[FrameOptionsConstants.Header] = FrameOptionsConstants.SameOrigin;
    return this;
}

/// 
/// Add X-Frame-Options ALLOW-FROM {uri} to all requests, where the uri is provided
/// The page can only be displayed in a frame on the specified origin.
/// 
/// The uri of the origin in which the page may be displayed in a frame
public SecurityHeadersBuilder AddFrameOptionsSameOrigin(string uri)
{
    this._policy.SetHeaders[FrameOptionsConstants.Header] = string.Format(CultureInfo.InvariantCulture, this.FrameOptionsAllowFromUri, uri);
    return this;
}

Каждый метод устанавливает для заголовка желаемое значение. Используя созданный мной объект политики, он добавляет заголовок, используя константу имени заголовка из наших классов констант, а затем константу нужного значения.

Удаление заголовка сервера является обычным явлением, поэтому у меня есть для этого специальный метод:

/// 
/// Removes the Server header from all responses
/// 
public SecurityHeadersBuilder RemoveServerHeader()
{
    this._policy.RemoveHeaders.Add(ServerConstants.Header);
    return this;
}

Что касается политики разрешений, я хочу разрешить все возможные разрешения для функций, поэтому я просто перебираю перечисление и добавляю все значения.

/// 
/// Add the permissions policy
/// 
/// 
public SecurityHeadersBuilder AddPermissionsPolicy()
{
    // Get all permissions policies
    var permissionPolicies = Enum.GetValues();

    // Loop through the policies
    var policy = (from permissionPolicy in permissionPolicies
                  select permissionPolicy.Value() into value
                  where !string.IsNullOrEmpty(value)
                  select $"{value}=()").ToList();

    this._policy.SetHeaders[PermissionsPolicyConstants.Header] = string.Join(',', policy.ToArray());
    return this;
}

Для ваших нужд вам может потребоваться создать другую версию этого метода, которая устанавливает только необходимые вам разрешения.

У меня есть специальный метод, который создает нашу политику по умолчанию:

/// 
/// Add default headers in accordance with most secure approach
/// 
public SecurityHeadersBuilder AddDefaultSecurePolicy()
{
    this.AddXssProtectionBlock();
    this.AddStrictTransportSecurityMaxAge();
    this.AddPermissionsPolicy();    
    this.AddContentTypeOptionsNoSniff();
    this.AddReferrerPolicyStrictOriginWhenCrossOrigin();
    this.AddXDownloadOptionsNoOpen();

    return this;
}

Здесь я вызываю только те методы, которые мне нужны для реализации моей политики. Обратите внимание, что здесь отсутствует Политика безопасности контента. Я вернусь к этому.

Наконец, у меня есть метод сборки, который возвращает политику:

/// 
/// Builds a new  using the entries added.
/// 
/// The constructed .
public SecurityHeadersPolicy Build() => this._policy;

Промежуточное ПО

Следующая часть, которая мне нужна, — это промежуточное программное обеспечение для создания и использования этих заголовков. Промежуточное программное обеспечение требует метода вызова, который выполняет всю работу. То есть он берет политики, созданные в сборщике, и добавляет их как фактические заголовки. Кроме того, здесь я добавляю Политику безопасности контента.

using System.Globalization;
using EPiServer.Framework.ClientResources;
using Hero.OptimizelyCMS.Foundation.Extensions;
using System.Web;
using Hero.OptimizelyCMS.Foundation.Utilities;

namespace Hero.OptimizelyCMS.Foundation.SecurityPolicy.SecurityMiddleware;

/// 
/// Middleware for setting security headers
/// 
public class SecurityHeadersMiddleware
{
    private readonly RequestDelegate _next;
    private readonly SecurityHeadersPolicy _policy;
    private readonly ICspNonceService _cspNonceService;

    public SecurityHeadersMiddleware(RequestDelegate next, SecurityHeadersPolicy policy, ICspNonceService cspNonceService)
    {
        this._next = next;
        this._policy = policy;
        this._cspNonceService = cspNonceService;
    }

    /// 
    /// Add and remove desired headers
    /// 
    /// The current HttpContext
    /// 
    public async Task Invoke(HttpContext context)
    {
        // Get existing headers
        var headersDictionary = context.Response.Headers;

        // If we should add Csp
        if (ShouldAddCsp(context))
        {
            // Add CSP
            headersDictionary[ContentSecurityPolicyConstants.Header] = this.GetContentSecurityPolicy();

            // Frame options
            headersDictionary[FrameOptionsConstants.Header] = FrameOptionsConstants.SameOrigin;
        }

        // Loop through headers to add
        foreach (var headerValuePair in this._policy.SetHeaders)
        {
            // Add each header
            headersDictionary[headerValuePair.Key] = headerValuePair.Value;
        }

        // Loop through headers to remove
        foreach (var header in this._policy.RemoveHeaders)
        {
            // Remove the header
            headersDictionary.Remove(header);
        }

        await this._next(context);
    }

    /// 
    /// Determine if we should add Csp
    /// 
    /// 
    /// 
    private static bool ShouldAddCsp(HttpContext context)
    {
        // Get the current url
        var currentUrl = context.Request.Url();
        var path = currentUrl.PathAndQuery;
        var segments = path.Split("http://world.optimizely.com/", StringSplitOptions.RemoveEmptyEntries);

        // Get the first segment
        var segmentZero = segments.ElementAtOrDefault(0);

        // If there is no segment zero, we are on the home page
        if (string.IsNullOrEmpty(segmentZero))
        {
            return true;
        }

        // If the segment is one of these, we should not add CSP
        return segmentZero.ToLower(CultureInfo.InvariantCulture) switch
        {
            "error" => false,
            "episerver" => false,
            "util" => false,
            "cleaner" => false,
            "redirectmanager" => false,
            _ => true,
        };
    }

    /// 
    /// Create the Csp
    /// 
    /// 
    public string GetContentSecurityPolicy()
    {
        var policy = new List();

        // Get Csp Enum values
        var contentSecurityPolicies = Enum.GetValues();

        // Loop through each policy
        foreach (var contentSecurityPolicy in contentSecurityPolicies)
        {
            // Get the policy value
            var value = contentSecurityPolicy.Value();

            // Add the nonce to the policy
            if (string.IsNullOrEmpty(value))
            {
                continue;
            }

            var nonce = this._cspNonceService.GetNonce();
            nonce = HttpUtility.JavaScriptStringEncode(nonce);
            value = value.Replace("{nonceValue}", nonce);
            policy.Add(value);
        }

        // Return the policy
        return string.Join(';', policy.ToArray());
    }
}

И давайте разберем, что здесь происходит:

Я внедряю RequestDelegate. Вот как промежуточное программное обеспечение перейдет к следующему фрагменту промежуточного программного обеспечения, как только оно будет выполнено здесь. Я добавляю нашу политику заголовка и экземпляр ICspNonceService для добавления политики безопасности контента nonce (подробнее об этом чуть позже).

При вызове Invoke я получаю текущий словарь заголовков, определяю, следует ли добавить политику безопасности контента, а затем просматриваю все соответствующие заголовки в нашей политике, добавляю их в ответ и удаляю все заголовки, которые следует удалить. .

Я обнаружил, что добавление Политики безопасности контента внутри CMS нарушило многие функции CMS. Поэтому я смотрю путь и не добавляю политику безопасности, если нахожусь в CMS. Сюда входят обычные пути cms (episerver, util), а также некоторые пользовательские URL-адреса для пользовательских инструментов администрирования. Возможно, вам придется указать другие пути в зависимости от установленных вами пользовательских инструментов или сторонних надстроек.

/// 
/// Determine if we should add Csp
/// 
/// 
/// 
private static bool ShouldAddCsp(HttpContext context)
{
    // Get the current url
    var currentUrl = context.Request.Url();
    var path = currentUrl.PathAndQuery;
    var segments = path.Split("http://world.optimizely.com/", StringSplitOptions.RemoveEmptyEntries);

    // Get the first segment
    var segmentZero = segments.ElementAtOrDefault(0);

    // If there is no segment zero, we are on the home page
    if (string.IsNullOrEmpty(segmentZero))
    {
        return true;
    }

    // If the segment is one of these, we should not add CSP
    return segmentZero.ToLower(CultureInfo.InvariantCulture) switch
    {
        "error" => false,
        "episerver" => false,
        "util" => false,
        "cleaner" => false,
        "redirectmanager" => false,
        _ => true,
    };
}

Если мне действительно нужно создать политику, я просматриваю настроенный мной Enum и добавляю эти значения.

/// 
/// Create the Csp
/// 
/// 
public string GetContentSecurityPolicy()
{
    var policy = new List();

    // Get Csp Enum values
    var contentSecurityPolicies = Enum.GetValues();

    // Loop through each policy
    foreach (var contentSecurityPolicy in contentSecurityPolicies)
    {
        // Get the policy value
        var value = contentSecurityPolicy.Value();

        // Add the nonce to the policy
        if (string.IsNullOrEmpty(value))
        {
            continue;
        }

        var nonce = this._cspNonceService.GetNonce();
        nonce = HttpUtility.JavaScriptStringEncode(nonce);
        value = value.Replace("{nonceValue}", nonce);
        policy.Add(value);
    }

    // Return the policy
    return string.Join(';', policy.ToArray());
}

Здесь следует отметить одну маленькую вещь. Я хочу использовать nonce со сценариями. Nonce — это хэш, уникальный для каждого запроса. Если тег сценария не имеет nonce, браузер сочтет его подозрительным и не запустит сценарий. Тег сценария с одноразовым номером может выглядеть так:

Я создал собственную реализацию этой службы и использую ее для генерации nonce и строки, заменяющей ее в атрибуте ScriptSource политики безопасности контента.

/// 
/// Concrete implementation of nonce service for generating the nonce
/// 
public class CspNonceService : ICspNonceService
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public CspNonceService(IHttpContextAccessor httpContextAccessor) => this._httpContextAccessor = httpContextAccessor;

    // Cache key
    public const string Key = "csp-nonce";

    /// 
    /// Get the current nonce
    /// 
    /// 
    public string GetNonce()
    {
        // Get the cache
        var items = this._httpContextAccessor.HttpContext?.Items;

        // If we have no items and the key is not in the cache
        if (items != null && !items.ContainsKey(Key))
        {
            // Generate the nonce and add it to cache
            items.Add(Key, GenerateNonce());
        }

        // Get the nonce from cache
        var nonce = items != null ? items[Key] : GenerateNonce();

        // Return the nonce
        return nonce?.ToString();
    }

    /// 
    /// Generate a new nonce
    /// 
    /// 
    private static string GenerateNonce()
    {
        // Generate a new nonce
        var numArray = new byte[32];
        using (var randomNumberGenerator = RandomNumberGenerator.Create())
        {
            randomNumberGenerator.GetBytes(numArray);
        }

        // Convert it to base 64 string
        return Convert.ToBase64String(numArray);
    }
}

Запускать

Последний шаг — зарегистрировать все необходимые мне ресурсы и добавить промежуточное ПО в конвейер.

В Startup.cs я регистрирую службу nonce в методе ConfigurationServices:

// Add nonce service
services.AddScoped(sp => new CspNonceService(sp.GetRequiredService()));

Optimizely может добавить это в свои собственные блоки скриптов после регистрации (https://docs.developers.optimizely.com/content-management-system/docs/content-security-policy).

ПРИМЕЧАНИЕ. Использование nonce — это предложение «все или ничего». Если вы это реализуете, всем тегам скриптов, добавленным на страницы, потребуется nonce. Для этого вам потребуется установить его вручную с помощью реализации ICspNonceService.

Затем я создаю метод расширения, чтобы упростить настройку этой политики.

/// 
/// Method for implementing security header middleware
/// 
public static class MiddlewareExtensions
{
    public static IApplicationBuilder UseSecurityHeadersMiddleware(this IApplicationBuilder app, SecurityHeadersBuilder builder)
    {
        var policy = builder.Build();
        return app.UseMiddleware(policy);
    }
}

Наконец, я добавляю промежуточное программное обеспечение в конвейер, также в Startup.cs в методе настройки. Я добавляю его на ранней стадии конвейера, чтобы все запросы получали его:

// Add security headers
app.UseSecurityHeadersMiddleware(new SecurityHeadersBuilder()
    .AddDefaultSecurePolicy()
    .RemoveHeader("X-Powered-By")
);

Это можно легко настроить в соответствии с вашими конкретными потребностями.

ПРИМЕЧАНИЕ. Я работаю локально, и вы можете видеть, что заголовок сервера все еще существует. Заголовок сервера для Kestrel необходимо удалить другим способом, когда вы настраиваете Kestrel в своем коде.

Теперь, когда я захожу на свой сайт, я вижу все свои заголовки:

И я вижу, что если я вхожу в CMS, политика безопасности контента не добавляется:

Наконец, если я просканирую сайт, используя эту конфигурацию, я увижу, что мои заголовки настроены правильно:

Потенциальное будущее усовершенствование: с помощью политики безопасности контента я создал довольно разрешительную политику. Фактически вы можете включить конкретные URL-адреса для различных частей политики. Вероятно, лучше всего сохранить это в настройках приложения, а затем извлечь и добавить в политику.

21 февраля 2024 г.

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.