Skip to main content

Node.jsアプリをCSRF攻撃から保護する方法

著者
Headshot of Victor Ikechukwu

Victor Ikechukwu

feature csrf node js

2023年10月17日

0 分で読めます

クロスサイトリクエストフォージェリ攻撃(CSRF)とは、Webブラウザーと正規のWebサイト間の信頼関係を悪用するセキュリティ上の脆弱性です。巧妙な攻撃者はブラウザーを操り、ユーザーが認証してログインしているWebサイトで悪意のある操作を実行させます。こうした攻撃は、ユーザーが偽装メールに添付されたリンクをクリックしたり、侵害されたWebサイトにアクセスしたりすることで始まることが多く、バックグラウンドで処理が実行されていることに気づかない場合があります。

CSRF攻撃が成功した場合、その影響は個人や企業の金銭的損失や評判の毀損から、ユーザーアカウントの侵害、不正な取引、さらには法的責任にまで及ぶ可能性があります。CSRF攻撃が進化し、巧妙化し続けるなか、Web開発者や組織はWebアプリケーションの完全性を守るため、堅牢な対策を実装する必要があります。

この記事では、Node.jsアプリケーションにおけるCSRF攻撃の仕組みと、その対策を解説します。実例を交えながら、実践的な手順やコード例、保護対策のテスト方法、Node.jsアプリケーションをCSRF攻撃から守るためのベストプラクティスを紹介します。

すぐに学習を始めたい方は、無料で実践的なCSRFレッスンをSnyk Learnでご利用いただけます。

CSRF攻撃を理解する

CSRF攻撃は、Webアプリケーションが認証済みユーザーのセッションに寄せる信頼を悪用します。攻撃者はユーザーを意図しない操作に誘導し、ユーザーに気づかれないまま機密データを操作したり、開示させたりできます。Node.jsアプリケーションをこうした脅威から守る方法を学ぶ前に、攻撃の基本的な仕組みと起こりうる影響を確認しましょう。

ユーザーはサイトにログインすると、再認証が必要になるまで、数日から数か月にわたってログイン状態を維持します。認証済みであるこの期間をセッションと呼びます。セッションが有効な間、サーバーはランダムで一意な識別子(トークン)、つまりセッションIDを生成し、そのセッションに関連付けます。続いて、サーバーはセッションIDをユーザーに返し、ブラウザーのCookie(セッション中にユーザーのブラウザーに紐付けられるデータ)に保存します。

その後、ユーザーがWebサイトの状態を変更するリクエスト(フォームの送信など)を行うたびに、セッションIDがリクエストとともに送信されます。サーバーはそのIDを、サーバー側のトークンと照合します。値が一致すればリクエストを許可し、アプリケーションが操作を実行します。一致しない場合はアクセスを拒否します。

典型的なCSRF攻撃では、攻撃者はログイン中の被害者に代わって操作を行うよう偽装した悪意あるリクエストを作成し、認証済みユーザーのセッションに対する信頼を悪用します。標的ユーザーがよく訪れるWebサイトやメールに有害なリンクを埋め込むなど、ソーシャルエンジニアリングの手法を使ってCSRF攻撃を実行できます。セキュリティ対策が不十分なWebアプリケーションは、有効な認証情報が付与されたリクエストを、正規のユーザー操作として誤って許可してしまうことがあります。

CSRF攻撃がもたらすリスクは深刻です。たとえば、2018年に発生したFacebookのセキュリティ侵害では、世界中で5,000万を超えるアカウントが影響を受けました。原因は、保護が不十分なアクセストークンでした。これらのトークンは、クライアント側リクエストの認証時にオリジンをまたいで悪用されるおそれがあります。十分なセキュリティ対策がなかったことで機密性の高いユーザーデータが侵害され、金銭的損失、評判の毀損、法的な影響が生じました。セキュリティ侵害は顧客の信頼を損ない、組織やそのサービスの評判を低下させる可能性があります。

CSRF攻撃が成功した場合の影響は、標的となるアプリケーションと、ユーザーに提供されている機能によって異なります。考えられる影響は次のとおりです。

  • データの改ざん — CSRF攻撃により、攻撃者がWebアプリケーション内の機密データを改ざんできる場合があります。ユーザープロフィールの設定変更、アカウントの設定や環境設定の変更、アクセス権限の範囲内で変更可能なデータの改ざんなどが考えられます。

  • 不正な操作 — 攻撃者はリクエストを偽造し、認証済みユーザーが許可していない操作を実行できます。たとえば、フォームの送信、金融取引、コンテンツの削除などです。

  • 情報漏えい — 攻撃者は脆弱性を悪用してユーザーをだまし、プライベートメッセージや金融情報などの機密データを漏えいさせることがあります。また、アプリケーション内に保存された独自のデータをユーザーに開示させることもできます。

  • アカウントの乗っ取り — 攻撃者は、ログイン情報の変更など、アカウントの認証情報を変更する操作をユーザーに行わせ、不正にアカウントへアクセスして支配できる場合があります。正規ユーザーになりすまして、制限されたリソースにアクセスしたり、アプリケーション内や他のユーザーに対してさらなる攻撃を仕掛けたりする可能性があります。

CSRF対策

Node.jsアプリケーションをCSRF攻撃から守る主な方法は次のとおりです。

シンクロナイザートークンパターン(STP)を使う

シンクロナイザートークンパターンでは、ユーザーセッションごとに一意のトークンを生成します。フォーム送信に埋め込むか、AJAXリクエストのカスタムヘッダー値またはJSONペイロードの一部として含めます。リクエストを受信すると、サーバーがこのトークンを検証します。

STPでは、ユーザーセッションごとに一意のランダムなトークン(CSRFトークン)を生成します。サーバーはHTMLやJSONなどのレスポンスペイロードの一部として、CSRFトークンをユーザーに送信します。以降の各リクエストでは、アプリケーションがリクエストヘッダーまたはカスタムPOSTパラメーターにトークンを含めます。サーバーは受信したトークンが存在し、ユーザーセッションのトークンと一致することを検証します。一致すれば、有効なセッションを持つ正規のユーザーによるリクエストです。

STPは実装が簡単で、一般的な攻撃ベクトルに対して効果的です。ただし、サーバー側で追加の状態管理が必要になる場合があります。STPでCSRFトークンの保存にCookieを使う場合は、__Host-をプレフィックスとして付け、Cookieのセキュリティを強化するとともに、設定したドメイン以外からアクセスできないようにします。

SameSite Cookieを使う方法では、セッションCookieにSameSite属性を設定し、対象Webサイトと同じドメインから発信されたリクエストにのみCookieを送信するようアプリケーションに指示します。この方法により、異なるオリジンからのリクエストにCookieが含まれるのを防ぎます。SameSite属性には、strictとlaxの2つの値を指定できます。

strictに設定すると、ブラウザーはファーストパーティのコンテキストでのみセッションCookieを送信します。ユーザーが別のWebサイトに移動した場合は送信されません。この方法により、悪意あるサードパーティーサイトからのCSRF攻撃を効果的に防げます。

laxに設定すると、ブラウザーはCookieを設定したWebサイト(ドメイン)と同じサイトからのリクエスト、およびトップレベルのGETリクエストにCookieを送信します。

最新のブラウザーではSameSite Cookie属性が自動的に適用されますが、古いブラウザーやモバイルアプリなどのWeb以外のクライアントではサポートが限られています。この制限により、CSRF攻撃への防御策としてのSameSite Cookieの有効性は大きく損なわれます。そのため開発者は、代替となるCSRF対策も検討し、幅広いクライアント環境で包括的に保護できるようにする必要があります。

Double Submit Cookieパターンでは、標準的なセッション識別子に加えて、一意のトークンを含むCookieを発行します。送信時に、このCookieをクライアント側のリクエストヘッダーまたはフォームデータに含めます。Double Submit Cookieパターンは、全体的なパフォーマンスへの影響を抑えながら、状態管理の負担を軽減します。そのため、多様なコンテンツ配信ネットワークやマイクロサービスアーキテクチャを備えたステートレスアプリケーションに適しています。

ただし、この方法では、二次トークンを保持するブラウザー側のストレージを標的とする高度な攻撃を防げません。攻撃ベクトルの一つがクロスサイトスクリプティング(XSS)です。XSS攻撃に対して脆弱なアプリケーションでは、攻撃者がCookieから一意のトークンを取得し、その後の悪意あるリクエストで使用できてしまいます。

また、ユーザーが適切なセキュリティ対策を講じていない場合、Double Submit Cookieパターンは中間者(MITM)攻撃に対して脆弱です。攻撃者はクライアントの最初のリクエストから元のCSRFトークンを傍受し、それを使って悪意あるリクエストを作成できます。

署名付きDouble Submit Cookieを使うと、Double Submit Cookieパターンをより堅牢にできます。この方法では、サーバーだけが知る秘密鍵を使うため、攻撃者が独自のCSRFトークンを生成して挿入することを防げます。

さらに強化するには、HTTP Strict-Transport-Security(HSTS)レスポンスヘッダーを適用し、__Host-などのCookieプレフィックスを使用します。ただし、この記事の執筆時点では、Cookieプレフィックスに対応していないブラウザーが25%あります。

Node.jsアプリでCSRF対策を実装する

アプリケーションをCSRF攻撃から守る方法を確認したところで、実装方法を見ていきましょう。手順を進める前に、次のものを用意してください。

ユーザーが他のユーザーに資金を送金できる、シンプルなNode.jsアプリを作成します。このアプリケーションでは、WebフレームワークのExpressを使用します。

まず、プロジェクト用のフォルダーを作成し、nodejs-csrf-strategiesという名前を付けます。ターミナルでこのフォルダーを開き、npm init -yコマンドを実行してNode.jsプロジェクトを初期化します。次に、npm i expressコマンドを実行してExpress Webフレームワークをインストールします。

index.jsという名前のファイルを作成し、以下のコードを貼り付けます。

// import dependencies
const express = require('express');

// create express app
const app = express();

// enable parsing of request body
app.use(express.urlencoded({ extended: false }));

// home route to render the form
app.get('/', (req, res) => {
 res.send(`
   <html>
     <body>
       <h1>CSRF Demo App</h1>
       <form method="POST" action="/transfer">
         <input type="text" name="amount" placeholder="Amount" /><br />
         <input type="submit" value="Transfer" />
       </form>
     </body>
   </html>
 `);
});

// process the transfer
app.post('/transfer', function (req, res) {
   // Perform the transfer operation
   // (in a real application, you would validate and process the data here)
   res.send('Successfully transferred the set amount')
})

// Start the server
app.listen(3000, () => {
   console.log('Server listening on port 3000');
});

このサンプルアプリケーションでは、/ルートが、送金する金額を入力するHTMLフォームを表示します。次に、/transferルートが、フォームに入力された金額の送金に成功したことを示すメッセージを返します。実際のアプリケーションでは、ここで送金処理を行います。このアプリケーションのコードにはCSRF攻撃に対する脆弱性があるため、本番環境では使用しないでください。

コンピューターにSnyk CLIをインストールしたら、Snyk Codeを有効にし、Snyk Codeでサンプルプロジェクトの脆弱性をスキャンします。Snyk CLIを使うと、Snyk Codeの機能を開発ワークフローに統合し、ローカルでSnyk Codeのテストを実行して、アプリケーションコードのセキュリティ脆弱性を確認できます。

ターミナルでプロジェクトフォルダーを開き、snyk code testコマンドを実行します。次のスクリーンショットのような出力が表示されます。機密情報の平文送信、計算コストが不十分なパスワードハッシュの使用、情報漏えい、CSRF、正規表現によるサービス拒否(ReDOS)、XSSに関する詳細が示されます。

CSRF、XSS、ReDoS、安全性の低いSHA1ハッシュ、平文での送信など、中程度および重大度の高い脆弱性を一覧表示したターミナルのセキュリティスキャン。

この結果は、アプリケーションがCSRF攻撃や一覧にあるその他の攻撃に対して脆弱であることを示しています。

Expressアプリを初期化する行(const app = express())にカーソルを合わせると、次のスクリーンショットのように、CSRF攻撃とその防止に役立つベストプラクティスについてSnykの詳細情報を確認できます。このリアルタイムの脆弱性検出機能はSnyk VS Code拡張機能によって実現されており、コードの記述中に脆弱性を発見できます。

CSRF保護が無効であることを示すSnykの警告と、脆弱な送金エンドポイントが表示されたExpressアプリのコードエディター。

Snyk CodeによってNode.jsアプリがCSRF攻撃に対して脆弱だと判明したところで、先ほど紹介したCSRF対策がどのように役立つかを見ていきましょう。

STPを使ってアプリを保護する

サンプルのNode.jsアプリでSTPを実装しましょう。まず、コマンド `npm install csurf` を実行してcsurfミドルウェアをインストールします。このミドルウェアを使うには、セッションミドルウェアまたはCookieパーサーを初期化する必要があります。ここではCookieパーサーを使用します。

コマンド npm install cookie-parser を実行して、Cookieパーサーをインストールします。

次に、JavaScriptファイルの先頭でミドルウェアとCookieパーサーをインポートします。

const cookieParser = require('cookie-parser')
const csrf = require('csurf')

Cookieの解析を有効にし、ルートミドルウェアを設定するには、GETルートの直前に次のコードを追加します。

app.use(cookieParser())

const csrfProtection = csrf({ cookie: true })

csrfProtectionのCookieオプションがtrueに設定されているため、Cookieを解析する必要があります。

次に、HTMLフォームを配信する際に、生成されたCSRFトークンを非表示の入力フィールドとして含めます。これにより、送信するHTMLフォームにCSRFトークンが埋め込まれます。GETルートのコードを次のコードに置き換えます。

app.get('/', csrfProtection, function (req, res) {
   // retrieves the CSRF token
   const csrfToken = req.csrfToken()

   // Render the form with the csrf token as a hidden field
   res.send(`
       <html>
       <head>
           <title>CSRF Demo</title>
       </head>
       <body>
           <h1>CSRF Demo</h1>
           <form action="/transfer" method="POST">
           <input type="text" name="amount" placeholder="Amount" required>
           <input type="submit" value="Transfer">
           <input type="hidden" name="_csrf" value="${csrfToken}">
           </form>
       </body>
       </html>
   `);
})

POSTルートを以下のコードに変更して、csrfProtectionミドルウェアを適用します。

app.post('/transfer', csrfProtection, function (req, res) {
   // Perform the transfer operation
   // (in a real application, you would validate and process the data here)
   res.send('Successfully transferred the set amount')
})

このミドルウェアは、受信したPOSTリクエストの_csrfフィールドの値を、アプリケーションがリクエストボディに送信した値と、ユーザーのセッションに保存されているCSRFトークンと照合して検証します。後者はユーザーのCookieに紐付けられています。

先ほど作成したNode.jsアプリにSameSite Cookieを実装しましょう。まず、npm install cookie-parserを実行してcookie-parserミドルウェアをインストールします。以下のコードを使ってcookie-parserミドルウェアをアプリにインポートし、秘密鍵で初期化します。

const cookieParser = require('cookie-parser');
// …
app.use(cookieParser('<your-secret-key>', {
   sameSite: 'strict'
}))

<your-secret-key>には、Cookieへの署名に使用する一意の文字列を設定します。これは、暗号学的に安全な方法で生成した、予測困難で十分な長さのランダムな値です。

一意の文字列を使ってCookieに署名すると、その内容がハッシュ化され、一意の署名が作成されます。後でサーバーがブラウザーからCookieを受け取った際、同じ秘密鍵を使って署名を確認することで、Cookieの整合性を検証できます。

この方法は、攻撃者がユーザーのセッションを盗んだり、なりすましたりするセッションハイジャックへの対策に役立ちます。攻撃者がCookieを改ざんすると署名が一致しなくなり、サーバーはCookieが改ざんされたことを検知できます。そのため、攻撃者はCookieに保存されたセッション関連データを変更できません。

Double Submit Cookieパターンを実装するには、cookie-parserをインストールします。次に、必要なパッケージをインポートしてCookieの解析を有効にするため、以下のコードをアプリに追加します。

const cookieParser = require('cookie-parser');
const crypto = require('crypto');

app.use(cookieParser());

次に、以下のコードを使ってCSRFトークンを生成・検証するミドルウェア関数を作成します。

// generate CSRF token middleware
function generateCSRFToken(req, res, next) {
 const csrfToken = crypto.randomBytes(16).toString('hex');
 console.log(csrfToken)
 res.cookie('mycsrfToken', csrfToken);
 req.csrfToken = csrfToken;
 next();
}

// validate CSRF token middleware
function validateCSRFToken(req, res, next) {
 const csrfToken = req.cookies.mycsrfToken;
 if (req.body.csrfToken === csrfToken) {
   next();
 } else {
   res.status(403).send('Invalid CSRF token');
 }
}

次に、generateCSRFTokenミドルウェアをGETルートに適用します。

 app.get('/', generateCSRFToken, (req, res) => {
 res.send(`
   <html>
     <body>
       <h1>CSRF Demo App</h1>
       <form method="POST" action="/transfer">
         <input type="hidden" name="csrfToken" value="${req.csrfToken}" />
         <input type="text" name="amount" placeholder="Amount" /><br />
         <input type="submit" value="Transfer" />
       </form>
     </body>
   </html>
 `);
});

最後に、validateCSRFTokenミドルウェア関数をPOSTルートに適用します。

app.post('/transfer', validateCSRFToken, (req, res) => {
 // Perform the transfer operation
 // (in a real application, you would validate and process the data here)
 res.send(`Successfully transferred the set amount`);
});

CSRF対策をテストする

CSRF対策のテストが重要な理由は次のとおりです。

  • 不正な操作を防ぐ — Node.jsアプリケーションや実装したその他の対策が、リクエストを適切に検証し、不正な操作を防いでいることを確認できます。

  • ユーザーデータとプライバシーを保護する — 機密データの漏えいや改ざんを狙うCSRF攻撃の際にも、ユーザーデータが安全に保護されることを確認できます。

  • セキュリティ基準に準拠する — Payment Card Industry Data Security Standard (PCI DSS)など、多くのセキュリティ基準や規制では、組織にCSRF対策の実装を義務付けています。CSRF対策をテストすることで、こうした基準への準拠を確認し、罰則や法的問題を回避できます。

  • 脆弱性を特定する — 攻撃をシミュレーションしてCSRF対策の堅牢性をテストすることで、悪意ある人物に悪用される前に脆弱性を特定し、対処できます。

このチュートリアルの冒頭にある、変更を加えていない元のindex.jsコードに対するCSRF攻撃をシミュレーションするには、独自のHTMLフォームを使用できます。次のHTMLフォームを見てみましょう。

<html>
<body>
  <h1>Welcome to the Lucky Prize Game!</h1>
  <p>Congratulations! You have won $100!</p>
  <p>Click the button below to claim your prize:</p>
  <form id="csrfForm" action="http://localhost:3000/transfer" method="POST">
	<input type="hidden" name="amount" value="100" />
	<input type="submit" value="Claim Prize" />
  </form>
  <script>
	document.getElementById('csrfForm').submit();
  </script>
</body>
</html>

この独自のHTMLフォームは、ユーザーに魅力的なメッセージを表示します。100ドルの賞金が当たったと伝え、ボタンをクリックして受け取るよう促します。同時に、非表示のフォームがユーザーに気づかれないまま、/transferエンドポイントに金額を送信します。HTMLフォーム内の<script>タグのコードは、ページの読み込み時にフォームを自動送信します。元のコードでは、サーバーがCSRFの脅威を検知できないため、送金が成功してしまいます。

実装したCSRF対策のひとつをテストするには、STP対策を適用してからサーバーを実行します。その後、独自のHTMLフォームを送信してみてください。サーバーでForbiddenError: invalid csrf tokenエラーが発生し、送金は処理されません。この結果から、CSRF対策が想定どおりに機能していることがわかります。

実装した各対策をテストするには、次のようにします。

  • STP — 正しいトークンがない、または有効期限が切れたトークンを使ってリクエストを作成し、サーバーが拒否するか確認します。

  • SameSite Cookie — 異なるドメインからクロスオリジンリクエストを実行します。セッションCookieがないために、サーバーが不正なアクセスを拒否するか確認します。

  • Double Submit Cookie — 送信データまたはヘッダーとセッションCookieでトークンが一致しないリクエストを送り、サーバーが適切に検証するか確認します。

次に、Snykを使って更新したコードの脆弱性をテストします。他の対策を実装したコードもテストできますが、ここではSTP対策を実装したコードに焦点を当てます。

snyk code testコマンドを実行して脆弱性を確認します。次のスクリーンショットのような結果が表示されます。

index.jsで中程度の深刻度の情報漏えいと、高深刻度のクロスサイトスクリプティング(XSS)の検出結果を示すターミナルレポート。

結果から、このコードには情報漏えいとXSSの脆弱性はあるものの、CSRFの脆弱性はないことがわかります。つまり、先ほど検出したCSRFの脆弱性を修正できたということです。

Node.jsアプリケーションのCSRF対策におけるベストプラクティス

上記の対策を実装するだけでなく、次のベストプラクティスを実践してCSRF対策を強化しましょう。

  • 依存関係とミドルウェアを定期的に更新し、セキュリティ設定を最新の状態に保ちます。Snyk Open Sourceなどのツールを使うと、古いコンポーネントを特定して修正を提案できるため、更新作業を効率化できます。

  • 信頼できるソースへのリクエストに制限するContent Security Policy(CSP)を実装し、攻撃経路となる可能性を減らします。

  • STPとSameSite Cookieを組み合わせたり、CSPの適用とDouble Submit Cookieを併用したりして、複数のCSRF対策手法を組み合わせます。単一の対策だけに頼らない多層的なアプローチにより、脆弱性を最小限に抑えられます。

次のステップ

CSRF攻撃とその影響を理解することは、Node.jsアプリケーションのセキュリティ確保に不可欠です。堅牢な対策を実装し、効果的にテストして、ベストプラクティスに従うことで、脅威に対するアプリケーションの防御力を高められます。

CSRFなどの脆弱性を定期的にテストし、堅牢なセキュリティ対策を実装することで、リスクを軽減し、不正な操作を防げます。プロアクティブなセキュリティ対策と最新の手法を活用して、安全なNode.jsアプリケーションを構築しましょう。悪意ある攻撃者に悪用される前に、脆弱性に対処できます。

Node.jsアプリケーションを保護するには、この記事で紹介した概念と対策を適用し、Snyk Codeを試してみてください。

CTFを始めよう

オンデマンドのバーチャル入門ワークショップで、CTFチャレンジの解き方を学びましょう。