コンテンツにスキップ

クリックジャッキング⚓︎

クリックジャッキングとは⚓︎

安全なウェブサイトの作り方 - 1.9 クリックジャッキング | 情報セキュリティ | IPA 独立行政法人 情報処理推進機構 よりクリックジャッキングの定義を以下に引用します。

ウェブサイトの中には、ログイン機能を設け、ログインしている利用者のみが使用可能な機能を提供しているものがあります。該当する機能がマウス操作のみで使用可能な場合、細工された外部サイトを閲覧し操作することにより、利用者が誤操作し、意図しない機能を実行させられる可能性があります。このような問題を「クリックジャッキングの脆弱性」と呼び、問題を悪用した攻撃を、「クリックジャッキング攻撃」と呼びます。

AlesInfiny Maris でのクリックジャッキング対策⚓︎

AlesInfiny Maris では、クリックジャッキング対策としてフレーム内表示を原則としてすべて禁止します。

具体的には、以下の方針を採用します。

  • 主要ブラウザー向けの対策として
    • Content-Security-Policy ヘッダーの frame-ancestors 'none' を設定
  • レガシーブラウザー向けの後方互換対策として
    • X-Frame-Options: DENY を併せて設定

これにより、 AlesInfiny Maris ではデフォルトで一切の埋め込み表示を許可しないセキュアな構成を実現します。 以降、各設定項目について説明します。

Content-Security-Policy : frame-ancestors⚓︎

HTTP レスポンスヘッダーに対して Content-Security-Policy ヘッダーフィールド frame-ancestors ディレクティブ を出力します。

frame-ancestors は、どのオリジンから当該コンテンツを <frame> 要素や <iframe> 要素、 <embed> 要素、 <object> 要素で読み込めるかを指定するためのディレクティブです。 以下のような特徴があります。

  • 複数オリジンの指定が可能
  • ワイルドカード指定が可能(※ AlesInfiny Maris では禁止)
  • 主要ブラウザーで広くサポート

以下に示す frame-ancestors の指定内容により、フレーム内表示の許可範囲が異なります。

設定値 表示できる範囲
frame-ancestors 'none'; すべてのオリジンからのフレーム内の表示を禁止する
frame-ancestors 'self'; 同一オリジンからのフレーム内の表示のみを許可する
frame-ancestors https://example.com; 指定したオリジンからのフレーム内の表示のみを許可する
frame-ancestors https://example.com https://sub.example.com; 複数の指定したオリジンからのフレーム内の表示を許可する

AlesInfiny Maris では、方針のとおり frame-ancestors 'none'; を設定します。

X-Frame-Options⚓︎

HTTP レスポンスヘッダーに対して X-Frame-Options ヘッダーフィールド を出力します。 これにより、他ドメインのサイトからの <frame> 要素や <iframe> 要素、 <embed> 要素、 <object> 要素による読み込みを制限します。

以下に示す指定内容により、フレーム内表示の許可範囲が異なります。

設定値 表示できる範囲
DENY すべてのドメインからのフレーム内の表示を禁止する
SAMEORIGIN 同一オリジンからのフレーム内の表示のみを許可する
ALLOW-FROM (非推奨) 指定したオリジンからのフレーム内の表示のみを許可する

かつてはクリックジャッキング対策の主流でしたが、以下の制約があります。

  • 指定可能なオリジンが限定的
  • ALLOW-FROM は主要なモダンブラウザーで互換性がない

このため、 X-Frame-Options はレガシーブラウザー向けの補助的な対策として位置づけます。

こちら に記載のとおり、より包括的な設定をする場合には Content-Security-Policy : frame-ancestors を使用するよう推奨されています。

このヘッダーで提供されるオプションよりも包括的な設定については、Content-Security-Policy ヘッダーの frame-ancestors ディレクティブを参照してください。

AlesInfiny Maris では、方針のとおり X-Frame-Options: DENY を設定します。

ブラウザーにおけるヘッダーの優先順位⚓︎

Content-Security-Policyframe-ancestorsX-Frame-Options の両方が設定されている場合、以下のような挙動になります。

  • モダンブラウザー
    • frame-ancestors を優先し、 X-Frame-Options を無視
  • frame-ancestors 非対応のレガシーブラウザー
    • X-Frame-Options にフォールバック

アプリケーションの設定⚓︎

AlesInfiny Maris では、frame-ancestors および X-Frame-Options を以下の方法で設定します。

  • CSR アプリケーション

    SPA アプリケーションを配信する Web サーバーにおいて、すべてのレスポンスに対して当該ヘッダーを付与します。

    ただし、 AlesInfiny Maris では、 Web API 側においても Program.cs にて API レスポンスヘッダーへ同様のヘッダーを付与します。 Web API は通常フレーム埋め込みの対象とはなりませんが、以下の理由により設定します。

    • セキュリティ設定の統一(標準化)
    • 将来的な構成変更時の安全性確保
    • セキュリティ監査対応の容易化

    なお、 コードが冗長化することを避けるため、一部処理を別クラスに切り出しています。

    Program.cs での HTTP レスポンスヘッダー設定例
    HttpSecurityHeadersMiddleware.cs
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    using Microsoft.AspNetCore.Http;
    
    namespace Dressca.Web.Extensions;
    
    /// <summary>
    /// HTTP レスポンスヘッダーにセキュリティ関連の設定を追加するミドルウェアです。
    /// </summary>
    public class HttpSecurityHeadersMiddleware
    {
        private readonly RequestDelegate next;
    
        /// <summary>
        /// <see cref="HttpSecurityHeadersMiddleware"/> クラスの新しいインスタンスを生成します。
        /// </summary>
        /// <param name="next">HTTP 要求を処理できる delegate</param>
        public HttpSecurityHeadersMiddleware(RequestDelegate next)
        {
            this.next = next;
        }
    
        /// <summary>
        /// <see cref="HttpSecurityHeadersMiddleware"/> のメイン ロジックを実行します。
        /// </summary>
        /// <param name="context">HTTP コンテキスト</param>
        /// <returns>パイプラインの次の処理</returns>
        public async Task InvokeAsync(HttpContext context)
        {
            context.Response.OnStarting(() =>
            {
                // コンテンツタイプを誤認識しないよう、HTTPレスポンスヘッダに「X-Content-Type-Options: nosniff」の設定を追加
                context.Response.Headers["X-Content-Type-Options"] = "nosniff";
    
                // クリックジャッキング攻撃への対策として、HTTP レスポンスヘッダに、「Content-Security-Policy」を「frame-ancestors 'none'」に設定
                context.Response.Headers.ContentSecurityPolicy = "frame-ancestors 'none'";
    
                // レガシーブラウザー向けのクリックジャッキング攻撃への対策として、HTTP レスポンスヘッダに、「X-FRAME-OPTIONS」を「DENY」に設定
                context.Response.Headers["X-Frame-Options"] = "DENY";
    
                return Task.CompletedTask;
            });
    
            await this.next(context);
        }
    }
    
    Program.cs (Dressca.Web.Consumer)
      1
      2
      3
      4
      5
      6
      7
      8
      9
     10
     11
     12
     13
     14
     15
     16
     17
     18
     19
     20
     21
     22
     23
     24
     25
     26
     27
     28
     29
     30
     31
     32
     33
     34
     35
     36
     37
     38
     39
     40
     41
     42
     43
     44
     45
     46
     47
     48
     49
     50
     51
     52
     53
     54
     55
     56
     57
     58
     59
     60
     61
     62
     63
     64
     65
     66
     67
     68
     69
     70
     71
     72
     73
     74
     75
     76
     77
     78
     79
     80
     81
     82
     83
     84
     85
     86
     87
     88
     89
     90
     91
     92
     93
     94
     95
     96
     97
     98
     99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    using System.Text.Json;
    using Dressca.ApplicationCore;
    using Dressca.EfInfrastructure;
    using Dressca.Store.Assets.StaticFiles;
    using Dressca.Web.Configuration;
    using Dressca.Web.Consumer;
    using Dressca.Web.Consumer.Baskets;
    using Dressca.Web.Consumer.Mapper;
    using Dressca.Web.Consumer.Resources;
    using Dressca.Web.Controllers;
    using Dressca.Web.Extensions;
    using Dressca.Web.HealthChecks;
    using Dressca.Web.Http;
    using Dressca.Web.Runtime;
    using Maris.Core.Text.Json;
    using Microsoft.AspNetCore.CookiePolicy;
    using Microsoft.AspNetCore.HttpLogging;
    using Microsoft.AspNetCore.Mvc.ApiExplorer;
    using Microsoft.Extensions.DependencyInjection.Extensions;
    using Microsoft.Extensions.Options;
    
    var builder = WebApplication.CreateBuilder(args);
    
    // アプリケーション設定ファイルの定義と型をバインドし、 DataAnnotation による検証を有効化する。
    builder.Services
        .AddOptions<WebServerOptions>()
        .BindConfiguration(nameof(WebServerOptions))
        .ValidateDataAnnotations()
        .ValidateOnStart();
    
    builder.Services.AddSingleton<ApplicationCookieBuilder>();
    
    // サービスコレクションに CORS を追加する。
    builder.Services.AddCors();
    
    // CookiePolicy を DI に登録(他のコードから IOptions<CookiePolicyOptions> で取得可能にする)
    builder.Services.AddOptions<CookiePolicyOptions>()
        .Configure<IOptions<WebServerOptions>>((cookiePolicy, webServerOptions) =>
        {
            // アプリケーション全体の Cookie ポリシーを定義する。
            cookiePolicy.HttpOnly = HttpOnlyPolicy.Always;
            cookiePolicy.Secure = CookieSecurePolicy.Always;
    
            cookiePolicy.MinimumSameSitePolicy =
                webServerOptions.Value.AllowedOrigins.Length > 0
                    ? SameSiteMode.None
                    : SameSiteMode.Strict;
        });
    
    builder.Services
        .AddControllers(options =>
        {
            options.Filters.Add<BuyerIdFilterAttribute>();
            if (builder.Environment.IsDevelopment())
            {
                options.Filters.Add<BusinessExceptionDevelopmentFilter>();
            }
            else
            {
                options.Filters.Add<BusinessExceptionFilter>();
            }
        })
        .ConfigureApiBehaviorOptions(options =>
        {
            options.SuppressMapClientErrors = true;
    
            // Bad Request となった場合の処理。
            var builtInFactory = options.InvalidModelStateResponseFactory;
            options.InvalidModelStateResponseFactory = context =>
            {
                // エラーの原因をログに出力。
                var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<Program>>();
                logger.LogInformation(Events.ReceiveHttpBadRequest, LogMessages.ReceiveHttpBadRequest, JsonSerializer.Serialize(context.ModelState, DefaultJsonSerializerOptions.GetInstance()));
    
                // ASP.NET Core の既定の実装を使ってレスポンスを返却。
                return builtInFactory(context);
            };
        });
    
    builder.Services.AddOpenApiDocument(config =>
    {
        config.PostProcess = document =>
        {
            document.Info.Version = "1.0.0";
            document.Info.Title = "Dressca Consumer Web API";
            document.Info.Description = "Dressca Consumer の Web API 仕様";
            document.Servers.Add(new()
            {
                Description = "ローカル開発用のサーバーです。",
                Url = "https://localhost:5001",
            });
        };
    });
    
    builder.Services.AddDresscaEfInfrastructure(builder.Configuration, builder.Environment);
    
    builder.Services.AddStaticFileAssetStore();
    
    builder.Services.AddDresscaApplicationCore();
    
    builder.Services.AddDresscaDtoMapper();
    
    if (builder.Environment.IsDevelopment())
    {
        builder.Services.AddHttpLogging(logging =>
        {
            logging.LoggingFields = HttpLoggingFields.All;
            logging.RequestBodyLogLimit = 4096;
            logging.ResponseBodyLogLimit = 4096;
        });
    }
    
    builder.Services.TryAddEnumerable(ServiceDescriptor.Transient<IApiDescriptionProvider, HealthCheckDescriptionProvider>());
    
    builder.Services.AddHealthChecks()
        .AddDresscaDbContextCheck("DresscaDatabaseHealthCheck");
    
    var app = builder.Build();
    
    if (app.Environment.IsDevelopment())
    {
        app.UseOpenApi();
        app.UseSwaggerUi();
        app.UseHttpLogging();
        app.UseExceptionHandler(ErrorController.DevelopmentErrorRoute);
    }
    else
    {
        app.UseExceptionHandler(ErrorController.ErrorRoute);
    }
    
    app.UseHttpsRedirection();
    
    app.UseSecuritySettings();
    
    app.UseStaticFiles();
    
    var options = app.Services.GetRequiredService<IOptions<WebServerOptions>>();
    
    // アプリケーション設定にオリジンの記述がある場合のみ CORS ポリシーを追加する。
    if (options.Value.AllowedOrigins.Length > 0)
    {
        app.UseCors(policy =>
        {
            // Origins, Methods, Header, Credentials すべての設定が必要(設定しないと CORS が動作しない)
            // レスポンスの Header を フロントエンド側 JavaScript で使用する場合、 WithExposedHeaders も必須
            policy
                .WithOrigins(options.Value.AllowedOrigins)
                .WithMethods("POST", "GET", "OPTIONS", "HEAD", "DELETE", "PUT")
                .AllowAnyHeader()
                .AllowCredentials()
                .WithExposedHeaders("Location");
        });
    }
    
    // DI に登録された CookiePolicyOptions を有効化する。
    app.UseCookiePolicy();
    
    app.UseAuthorization();
    
    app.MapControllers();
    
    app.MapHealthChecks(HealthCheckDescriptionProvider.HealthCheckRelativePath);
    
    app.MapFallbackToFile("/index.html");
    
    app.Run();
    
  • SSR アプリケーション

    Web アプリケーションプロジェクトの Program.cs で設定します。 なお、 コードが冗長化することを避けるため、一部処理を別クラスに切り出しています。

    Program.cs での HTTP レスポンスヘッダー設定例
    Program.cs
      1
      2
      3
      4
      5
      6
      7
      8
      9
     10
     11
     12
     13
     14
     15
     16
     17
     18
     19
     20
     21
     22
     23
     24
     25
     26
     27
     28
     29
     30
     31
     32
     33
     34
     35
     36
     37
     38
     39
     40
     41
     42
     43
     44
     45
     46
     47
     48
     49
     50
     51
     52
     53
     54
     55
     56
     57
     58
     59
     60
     61
     62
     63
     64
     65
     66
     67
     68
     69
     70
     71
     72
     73
     74
     75
     76
     77
     78
     79
     80
     81
     82
     83
     84
     85
     86
     87
     88
     89
     90
     91
     92
     93
     94
     95
     96
     97
     98
     99
    100
    101
    102
    103
    104
    105
    using DresscaCMS.Announcement;
    using DresscaCMS.Authentication;
    using DresscaCMS.Authentication.Infrastructures;
    using DresscaCMS.Web.Components;
    using DresscaCMS.Web.Components.Account;
    using DresscaCMS.Web.Extensions;
    using Microsoft.AspNetCore.Components.Authorization;
    using Microsoft.AspNetCore.Http.Features;
    using Microsoft.AspNetCore.HttpLogging;
    using Microsoft.FluentUI.AspNetCore.Components;
    
    var builder = WebApplication.CreateBuilder(args);
    
    var maxRequestBodySizeBytes = builder.Configuration.GetValue<long?>("MaxRequestSize:MaxRequestBodySizeBytes");
    if (maxRequestBodySizeBytes.HasValue)
    {
        // Kestrel サーバーのリクエストボディサイズの上限を設定
        builder.WebHost.ConfigureKestrel(options =>
        {
            options.Limits.MaxRequestBodySize = maxRequestBodySizeBytes;
        });
    }
    
    var multipartBodyLengthLimit = builder.Configuration.GetValue<long?>("MaxRequestSize:MultipartBodyLengthLimit");
    if (multipartBodyLengthLimit.HasValue)
    {
        // フォームオプションのマルチパートボディサイズの上限を設定
        builder.Services.Configure<FormOptions>(options =>
        {
            options.MultipartBodyLengthLimit = multipartBodyLengthLimit.Value;
        });
    }
    
    // Add services to the container.
    builder.Services.AddRazorComponents()
        .AddInteractiveServerComponents();
    builder.Services.AddFluentUIComponents();
    builder.Services.AddRazorPages();
    builder.Services.AddInMemoryStateStore();
    
    // お知らせメッセージに関するサービス一式を登録
    builder.Services.AddAnnouncementsServices(
        builder.Configuration,
        builder.Environment);
    
    // 認証に関するサービス一式を登録
    builder.Services.AddAuthenticationServices(
        builder.Configuration,
        builder.Environment);
    
    // 入れ子になったオブジェクトのバリデーションをサポートするためのサービスを登録
    builder.Services.AddValidation();
    
    if (builder.Environment.IsDevelopment())
    {
        builder.Services.AddHttpLogging(logging =>
        {
            // どのデータをどのくらいの量出力するか設定。
            // 適宜設定値は変更する。
            logging.LoggingFields = HttpLoggingFields.All;
            logging.RequestBodyLogLimit = 4096;
            logging.ResponseBodyLogLimit = 4096;
        });
    }
    
    // Blazor に依存した認証に関するサービスを登録
    builder.Services.AddCascadingAuthenticationState();
    builder.Services.AddScoped<IdentityRedirectManager>();
    builder.Services.AddScoped<AuthenticationStateProvider, IdentityRevalidatingAuthenticationStateProvider>();
    
    var app = builder.Build();
    
    if (app.Environment.IsDevelopment())
    {
        // HTTP 通信ログを有効にする。
        app.UseHttpLogging();
        await AuthenticationDbContextSeed.SeedAsync(app.Services);
    }
    
    if (!app.Environment.IsDevelopment())
    {
        app.UseExceptionHandler("/ServerError", createScopeForErrors: true);
    
        // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
        app.UseHsts();
    }
    
    app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true);
    app.UseHttpsRedirection();
    
    app.UseAntiforgery();
    
    app.MapStaticAssets();
    
    app.MapRazorPages();
    app.UseAuthorization();
    
    // クリックジャッキング攻撃への対策として、 CSP frame-ancestors を設定
    app.MapRazorComponents<App>()
        .AddInteractiveServerRenderMode(o => o.ContentSecurityFrameAncestorsPolicy = "'none'");
    
    // HTTP レスポンスヘッダーにセキュリティ関連の設定を追加するミドルウェアを使用
    app.UseSecuritySettings();
    
    app.Run();
    
    HttpSecurityHeadersMiddleware.cs
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    namespace DresscaCMS.Web.Extensions;
    
    /// <summary>
    /// HTTP レスポンスヘッダーにセキュリティ関連の設定を追加するミドルウェアです。
    /// </summary>
    public class HttpSecurityHeadersMiddleware
    {
        private readonly RequestDelegate next;
    
        /// <summary>
        /// <see cref="HttpSecurityHeadersMiddleware"/> クラスの新しいインスタンスを生成します。
        /// </summary>
        /// <param name="next">HTTP 要求を処理できる delegate</param>
        public HttpSecurityHeadersMiddleware(RequestDelegate next)
        {
            this.next = next;
        }
    
        /// <summary>
        /// <see cref="HttpSecurityHeadersMiddleware"/> のメイン ロジックを実行します。
        /// </summary>
        /// <param name="context">HTTP コンテキスト</param>
        /// <returns>パイプラインの次の処理</returns>
        public async Task InvokeAsync(HttpContext context)
        {
            context.Response.OnStarting(() =>
            {
                // コンテンツタイプを誤認識しないよう、HTTPレスポンスヘッダに「X-Content-Type-Options: nosniff」の設定を追加
                context.Response.Headers["X-Content-Type-Options"] = "nosniff";
    
                // レガシーブラウザー向けのクリックジャッキング攻撃への対策として、HTTP レスポンスヘッダに、「X-FRAME-OPTIONS」を「DENY」に設定
                context.Response.Headers["X-Frame-Options"] = "DENY";
    
                return Task.CompletedTask;
            });
    
            await this.next(context);
        }
    }
    

制限変更の方法⚓︎

前述のとおり、 AlesInfiny Maris ではクリックジャッキング対策としてデフォルトでフレーム内表示をすべて禁止する方針を採用しています。 ただし、業務要件上正当な理由で <iframe> 要素等の埋め込みが必要となる場合に限り、以下のような制限の変更を検討します。

同一オリジン内での埋め込みが必要な場合⚓︎

  • 同一オリジン内において、複数の Web リソースをフレーム要素等で構成する設計が採用されている場合
  • 同一オリジン内の別パスに配置されたコンテンツを、フレーム要素等を用いて表示する必要がある場合

このような場合には、同一オリジンからの埋め込みのみを許可します。

ヘッダー 設定値
Content-Security-Policy frame-ancestors 'self';
X-Frame-Options SAMEORIGIN

特定の外部オリジンからの埋め込みが必要な場合⚓︎

  • 信頼境界内にある特定の外部オリジンから、フレーム要素等を用いた表示を許可する必要がある場合
  • 信頼された別オリジンの Web リソースと、画面統合する設計が採用されている場合

このような場合には、許可するオリジンを明示的に列挙します。

ヘッダー 設定値
Content-Security-Policy frame-ancestors https://example.com;
X-Frame-Options DENY

X-Frame-Options : ALLOW-FROM はブラウザー互換性の問題があるため使用せず、 X-Frame-Options : DENY に設定します。 これにより、 Content-Security-Policy に対応のブラウザーは frame-ancestors により埋め込みが許可され、非対応のブラウザーには埋め込みを許さないようになります。 なお、ワイルドカードによる埋め込み許可は禁止します。