🔥

Next.jsアプリケーションのセキュリティ対策

2024/01/21に公開
2

Next.jsというフレームワークを使って、趣味でWebアプリケーションを作っています。

必要なセキュリティ対策について調べて、自分が実践したものについて書きます。今回はNext.jsのコードに組み込まれるセキュリティ対策のみ扱います。たとえば、インフラストラクチャレベルで行われるDDos対策などは今回は扱いません。

フレームワークのバージョン:

"dependencies": {
  "next": "14.0.4",
  "react": "^18",
  "react-dom": "^18",
  "sass": "^1.69.5"
},

HTTPヘッダーをカスタムしてセキュアにする

next.config.jsというファイルを編集すると、レスポンスのHTTPヘッダーをカスタマイズすることができます。これはパスごとに設定することができるようです。

https://nextjs.org/docs/pages/api-reference/next-config-js/headers

今回講じたセキュリティ対策は、コードベースで見るとnext.config.jsの編集でほぼ完結しました。カスタマイズしたHTTPヘッダーは以下の4つです。

  • Strict-Transport-Security
  • X-Frame-Options
  • Referrer-Policy
  • Content-Security-Policy

next.config.jsのサンプルは次のようになります。

/** @type {import('next').NextConfig} */

const cspHeader = `
    default-src 'self';
    script-src 'self' 'unsafe-eval' 'unsafe-inline';
    style-src 'self' 'unsafe-inline';
    object-src 'none';
    frame-ancestors 'none';
    block-all-mixed-content;
    upgrade-insecure-requests;
`;

const nextConfig = {
  async headers() {
    return [
      {
        source: "/(.*)",
        headers: [
          {
            key: "Strict-Transport-Security",
            value: "max-age=63072000; includeSubDomains; preload",
          },
          {
            key: "X-Frame-Options",
            value: "SAMEORIGIN",
          },
          {
            key: "Referrer-Policy",
            value: "strict-origin-when-cross-origin",
          },
          {
            key: "Content-Security-Policy",
            value: cspHeader.replace(/\n/g, ""),
          },
        ],
      },
    ];
  },
};

module.exports = nextConfig;

Strict-Transport-Securityの設定

Strict-Transport-SecurityというHTTPヘッダーは、HSTS(HTTP Strict Transport Security)と略されることもあります。Webサイトがブラウザに対し、HTTPSを用いた通信を強制するためのものです。

https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Strict-Transport-Security

HTTPで接続を受け付けたとき、HTTPSにリダイレクトされるようになっていると、ユーザはリダイレクトされる前に暗号化されていないサイトと通信する可能性があります。Strict-Transport-Securityを使用すると、ユーザが誤ってHTTP接続を試みた場合は、Webサイトを読み込まず、自動的にHTTPSリクエストに変換するように指示することができます。

Strict-Transport-Securityの設定方法は、以下のページに説明があります。

https://nextjs.org/docs/pages/api-reference/next-config-js/headers#strict-transport-security

Strict-Transport-Securityを設定すると、以下の攻撃の対策になります。

  • 中間者攻撃(Man-in-the-Middle Attack): 攻撃者がユーザとサーバの間に入り込み、データの盗聴や改竄を行う
  • SSLストリッピング: HTTPS接続を強制的にHTTP接続にダウングレードする

X-Frame-Optionsの設定

X-Frame-OptionsというHTTPヘッダーは、ブラウザがページを <iframe>等のHTML要素内に表示することを許可するかどうかを示すために使用されます。ただし、ユーザがX-Frame-Optionsに対応したブラウザを使用していないと有効にはならないようです。

https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/X-Frame-Options

Next.jsでこのHTTPヘッダーを設定する方法は、以下のページに説明があります。
https://nextjs.org/docs/pages/api-reference/next-config-js/headers#x-frame-options

X-Frameとは、<frame><iframe>などのHTML要素を総称しているのだと思います。自分は知らなかったのですが、これらはクリックジャッキングという攻撃の標的にされるようです。

クリックジャッキングとは、ユーザを意図しない操作(クリック)に誘導して、退会処理や投稿の公開範囲の変更などをさせる攻撃です。WebサイトAが、悪意ある他のWebサイトBの<iframe>から読み込まれ、ユーザはWebサイトBを操作しているつもりでも、実はWebサイトAで重要な操作を実行してしまう、というものです。

以下のIPAのページに詳しい説明がありました。
https://www.ipa.go.jp/security/vuln/websecurity/clickjacking.html

X-Frame-OptionsをSAMEORIGINに設定すると、自身と同じオリジンのフレーム内でのみ読み込みを許可できます。これにより、他のWebサイト上で勝手にページが表示されることを防ぐことができるため、クリックジャッキング攻撃の防止になります。

Referrer-Policyの設定

Referrer-Policyは、HTTPヘッダーのひとつであるRefererの送信を、どの範囲で許可するかを指示するためのHTTPヘッダーです。

refererというのは流入元のURLのことです。たとえば、メールアドレスとパスワードを含むhttps://example.com/auth?email=example@gmail.com&password=abcd1234のようなURLがあるとすると、こういったものが外部のWebサイトに遷移したときにrefererとして送信されてしまうことはリスクになります。

Referrer-Policyに設定可能なオプションは、こちらのページにありました。
https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Referrer-Policy

Next.jsで設定する方法はこちらのページで説明されています。
https://nextjs.org/docs/pages/api-reference/next-config-js/headers

strict-origin-when-cross-originという設定をするのがベストプラクティスである、という説明がありました。この設定にしておくと、次のような挙動になります。

  • 遷移先が同一オリジンの場合のみ完全なURLを送信する
  • 遷移先が外部サイトの場合、HTTPS接続であればURLのオリジン部分のみを送信する
  • 遷移先が外部サイトでHTTP接続の場合、refererは送信しない

余談ですが、流入元を示すRefererとReferrer-PolicyのReferrerのスペルが違うことが気になって調べました。どうやらRefererはスペルミスで、Referrer-Policyはスペルミスしなかったので、不一致が生じているようです。

Content-Security-Policyの設定

コンテンツセキュリティポリシーはCSPとも略されます。このHTTPヘッダーは、 ブラウザがスクリプトやスタイルシートなどのリソースを読み込むときに、許可するオリジンを指定するために使用します。たとえば、外部サイトのスクリプトの読み込みをブロックしたりすることができます。

Next.jsでコンテンツセキュリティポリシーを設定する場合、nonceを使用してインラインスクリプトの正当性を確認するのが良さそうです。nonceというのは、サーバーサイドで生成したランダムな文字列で、これをヘッダーとインラインスクリプトの両方に埋め込むことで検証します。

nonceを使用する場合の実装については、以下のページに詳しい説明がありました。
https://nextjs.org/docs/app/building-your-application/configuring/content-security-policy

コンテンツセキュリティポリシーの設定は、クロスサイトスクリプティングやクリックジャッキング、その他のインジェクション攻撃に対するセキュリティ対策として有効です。

インラインスクリプトや外部ドメインのスクリプトの読み込みを禁止することで、クロスサイトスクリプティングのリスクを減らすことができます。また、frame-ancestorsディレクティブを使用することで、Webページが外部サイトのiframeに埋め込まれるのを防ぐことができ、これはクリックジャッキング攻撃のリスクを減らすことができます。

スタイルシートの読み込みを制限しなければならない理由は知らなかったのですが、expression()関数を使用してスタイルシートからスクリプトを埋め込むことも可能なようです。コンテンツセキュリティポリシーでスタイルシートの読み込みを制限することで、その脆弱性の解消にもつながります。

その他のセキュリティ対策

最後にHTTPヘッダーのカスタマイズ以外のセキュリティ対策について軽く書きます。

  • クロスサイトスクリプティング対策
    • dangerouslySetInnerHTMLの使用を避ける
  • CSRF(クロスサイトリクエストフォージェリ)対策
    • CSRFトークンでフォームの正当性を検証する
  • SQLインジェクション対策
    • SQLを組み立てるときは文字列連結ではなくプレースホルダを使用する

CSRF対策やSQLインジェクション対策は、Next.jsに特別な話ではないので省略します。

dangerouslySetInnerHTMLについては今回調べるまで自分は知りませんでした。

Reactはデフォルトでクロスサイトスクリプティング対策が講じられているのですが、それをバイパスする手段がdangerouslySetInnerHTMLらしいです。Reactは通常JSXに埋め込まれた値をエスケープしますが、次のように書くとHTMLとして解釈できるようになります。
※動作確認はしていません

import React from 'react';

function DangerousComponent({ htmlContent }) {
  return <div dangerouslySetInnerHTML={{ __html: htmlContent }} />;
}

export default DangerousComponent;

このdangerouslySetInnerHTMLの使用は極力避けたほうが良いとされています。
https://legacy.reactjs.org/docs/introducing-jsx.html#jsx-prevents-injection-attacks

Discussion

kazuhokazuho

素晴らしい記事をありがとうございます!
とても参考になりました。

一点、Referrer-Policyの説明でリンク先が両方MDNになっているようでしたのでお伝えしておきます。

yutoo89yutoo89

コメントいただきありがとうございます!初めてなので、とても励みになります。
リンクのご指摘もありがとうございます🙇失礼いたしました、修正しておきます。