日本語版
最新ニュース
企業

独自のセキュリティヘッダーをロールアップする

Optimizely を活用した Web サイトには、適切なセキュリティ ヘッダーが必須です。 これに役立つさまざまなツールが世に出ていますが、特にセキュリティ ヘッダーなどの低レベルの機能については、可能であれば独自のソリューションを展開することを好みます。 NuGet パッケージを最新の状態に保つ必要はありません。 定期的なバージョン更新について心配する必要はありません。 それはすべて、私が管理するコードベースに含まれています。 Startup.cs でセットアップできるいくつかのカスタム ミドルウェアを使用してこれを実行しました。 これにより追加される可能性のあるヘッダーは次のとおりです。 X-コンテンツタイプ-オプション X フレーム オプション サーバ 厳格な輸送セキュリティ X-XSS 保護 リファラーポリシー アクセス許可ポリシー コンテンツセキュリティポリシー X-ダウンロード-オプション 最初に行う必要があるのは、必要なヘッダー名と値をすべて含む定数クラスを作成することです。 各クラスには、ヘッダー名の文字列とオプションの文字列が含まれています。 コンテンツ セキュリティ ポリシーとアクセス許可ポリシーはもう少し複雑なので、いくつかのオプションをカバーする列挙型も必要です。 X-コンテンツタイプ-オプション /// /// X-Content-Type-Options-related constants. ///…

独自のセキュリティヘッダーをロールアップする

1708626595
2024-02-21 19:49:23

Optimizely を活用した Web サイトには、適切なセキュリティ ヘッダーが必須です。 これに役立つさまざまなツールが世に出ていますが、特にセキュリティ ヘッダーなどの低レベルの機能については、可能であれば独自のソリューションを展開することを好みます。 NuGet パッケージを最新の状態に保つ必要はありません。 定期的なバージョン更新について心配する必要はありません。 それはすべて、私が管理するコードベースに含まれています。 Startup.cs でセットアップできるいくつかのカスタム ミドルウェアを使用してこれを実行しました。 これにより追加される可能性のあるヘッダーは次のとおりです。

  • X-コンテンツタイプ-オプション
  • X フレーム オプション
  • サーバ
  • 厳格な輸送セキュリティ
  • X-XSS 保護
  • リファラーポリシー
  • アクセス許可ポリシー
  • コンテンツセキュリティポリシー
  • X-ダウンロード-オプション

最初に行う必要があるのは、必要なヘッダー名と値をすべて含む定数クラスを作成することです。 各クラスには、ヘッダー名の文字列とオプションの文字列が含まれています。 コンテンツ セキュリティ ポリシーとアクセス許可ポリシーはもう少し複雑なので、いくつかのオプションをカバーする列挙型も必要です。

X-コンテンツタイプ-オプション

/// 
/// 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 フレーム オプション

/// 
/// 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 内で行われます。 このビルダーには、さまざまなヘッダー値を設定するためのさまざまなメソッドがあります。 次に、セキュリティ ポリシーを構築するメソッドを作成します。 概念的にはさまざまなポリシーを設定できます。 必要なのは 1 つだけなので、すべてのヘッダーを作成するデフォルトのメソッドを用意しています。 それらをポリシー クラスに保存します。

まずはポリシークラス。 これは非常に簡単で、ヘッダーを設定するための追加および削除メソッドがあるだけです。

/// 
/// 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 年を秒単位で保存する定数を作成します (Strinct Transport Security ヘッダーの場合、最大有効期間を 1 年に設定します)。

/// 
/// 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 の場合、さまざまな値を設定するための 3 つの方法があります。

/// 
/// 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;
}

ここでは、ポリシーを作成するために必要なメソッドだけを呼び出しています。 ここにはコンテンツ セキュリティ ポリシーが存在しないことに注意してください。 話は戻ります。

最後に、ポリシーを返す「build」メソッドがあります。

/// 
/// 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 を注入します。 このようにして、ミドルウェアはここでの処理が完了すると次のミドルウェアに進むことになります。 ヘッダー ポリシーと、コンテンツ セキュリティ ポリシー nonce を追加するための ICspNonceService のインスタンスを追加します (これについては後で詳しく説明します)。

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,
    };
}

実際にポリシーを構築する必要がある場合は、設定した列挙型をループしてこれらの値を追加します。

/// 
/// 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());
}

ここで注意すべき小さなことが 1 つあります。 スクリプトで nonce を使用したいと考えています。 nonce は、リクエストごとに一意のハッシュです。 スクリプト タグにナンスがない場合、ブラウザは疑わしいものとして扱い、スクリプトを実行しません。 nonce を含む script タグは次のようになります。

このサービスのカスタム実装を作成し、これを使用してノンスを生成し、コンテンツ セキュリティ ポリシーの 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 で、ConfigureServices メソッドに nonce サービスを登録します。

// 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);
    }
}

最後に、同じくconfigureメソッドのStartup.csにミドルウェアをパイプラインに追加します。 すべてのリクエストがそれを取得できるように、パイプラインの早い段階でこれを追加します。

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

これは、特定のニーズに合わせて簡単にカスタマイズできます。

注: 私はローカルで実行していますが、サーバー ヘッダーがまだそこにあることがわかります。 コード内で Kestrel を構成する場合は、Kestrel の Server ヘッダーを別の方法で削除する必要があります。

これで、自分のサイトにアクセスすると、すべてのヘッダーが表示されます。

CMS にログインすると、コンテンツ セキュリティ ポリシーが追加されていないことがわかります。

最後に、この構成を使用してサイトをスキャンすると、ヘッダーが適切に設定されていることがわかります。

将来の拡張の可能性: コンテンツ セキュリティ ポリシーを使用して、ここではかなり寛容なポリシーを構築しました。 実際には、ポリシーのさまざまな部分に特定の URL を含めることができます。 これはおそらく、appsettings で維持し、取得してポリシーに追加するのが最善です。

2024 年 2 月 21 日

#独自のセキュリティヘッダーをロールアップする

執筆者について: nipponese

Nipponese News編集部は、国内外のニュースを日本語で分かりやすくお届けします。