Skip to main content

Node.jsにおけるクロスオリジンリソース共有(CORS)のセキュリティ上の影響

著者
Headshot of Victor Ikechukwu

Victor Ikechukwu

blog feature cors

2023年9月13日

0 分で読めます

最新のWebアプリケーションでは、クロスオリジンリソース共有(CORS)によって、異なるオリジンでホストされているアプリケーション間の安全な通信が可能になります。開発者はCORSを使って、自分のアプリケーションから別のアプリケーションのサービスにアクセスできます。この方法なら、機能を一から作り直す必要がなく、開発期間を短縮し、開発者体験を向上させられます。

CORSは便利ですが、実装を誤ると、データ漏えいやサードパーティのWebサイトからの不正アクセスなど、Node.jsアプリケーションがセキュリティリスクにさらされる可能性があります。設定ミスによって意図しないオリジンに機密データが漏れたり、悪意あるWebサイトが同一オリジンポリシー(SOP)の保護を回避できたりするおそれがあります。

こうした脆弱性を理解し、CORSを安全に実装するためのベストプラクティスを取り入れることで、リスクを軽減できるだけでなく、アプリケーションの機能を維持しやすくなります。

この記事では、まずCORSとは何か、どのようなユースケースがあるのかを説明します。次に、コードサンプルを使ってNode.jsアプリケーションにCORSを実装します。潜在的なセキュリティ上の懸念点を確認した後、CORSを使用する際のベストプラクティスを紹介し、サンプルのセキュリティをテストします。読み進めるには、JavaScriptとNode.jsの基本的な経験があれば十分です。

CORSとそのユースケースを理解する

Webブラウザーは、Webアプリケーション間のやり取りを制御するために、同一オリジンポリシー(SOP)と呼ばれるセキュリティメカニズムを使用します。SOPは、あるオリジンでホストされているアプリケーションが、別のオリジンにあるアプリケーションのリソースを読み取ることを制限します。

この仕組みは、悪意のあるサイトが他のサイトのデータを読み取るのを防ぎますが、正当な用途まで制限してしまうことがあります。たとえば、アプリケーションに天気情報を取り込んだり、WebページにYouTube動画を埋め込んだりしたい場合はどうでしょうか。こうした公開リソースは誰でも読み取れるようにすべきですが、SOPによってブロックされます。

CORSを使うと、WebアプリケーションはSOPの制限を回避して、さまざまなWebサービス間で通信し、クロスオリジンリクエストを送信できます。アプリケーションはHTTPメソッドOPTIONSを使って、別のオリジンにあるリソースへプリフライトリクエストを送信できます。ブラウザーはこのリクエストを通じて、相手のオリジンのサーバーがリクエストを許可するかどうかを確認します。その結果に応じて、ブラウザーはSOPを適用するか、適用しないかを判断します。

CORSの一般的なユースケースには、コンテンツデリバリーネットワーク(CDN)からのリソース読み込み、複数のドメインにまたがるAPIリクエスト、サードパーティとの連携などがあります。

ただし、アプリケーションでCORSを適切に実装しないと、セキュリティリスクが生じます。ユーザーをだましてアプリケーション上で意図しない操作を実行させるクロスサイトリクエストフォージェリ(CSRF)攻撃を受けるおそれがあります。また、許可範囲が広すぎる設定では、オリジン間でユーザーデータや機密情報への不正アクセスを許してしまい、データ漏えいや、攻撃者に悪用される脆弱性につながる可能性があります。

たとえば、HTTPヘッダーAccess-Control-Allow-Originの値に、アスタリスク(*)で表されるすべてを許可するワイルドカードを使いたくなるかもしれません。このCORS設定では、あらゆるオリジンからアプリケーションへのアクセスが許可され、脆弱性につながるおそれがあります。代わりに、許可範囲を限定した具体的な設定を使用しましょう。許可するオリジンを動的に設定する場合は、より安全な方法として環境変数の利用を検討してください。

Node.jsアプリケーションでCORSを安全に実装する

このセクションでは、Node.jsアプリケーションでCORSを安全に実装する方法を順を追って説明します。WebフレームワークにExpress.jsを使ったシンプルなNode.jsアプリケーションを例にします。このアプリケーションは、架空のオンライン書店のAPIとして動作します。

このAPIには書籍のfetch、add、update、deleteを行う複数のエンドポイントがありますが、この例では書籍の取得のみを実装します。簡潔にするため、データストレージとして機能するsampleBooksDataオブジェクトをAPIから利用します。

前提条件

このチュートリアルを進めるには、以下が必要です。

  • JavaScriptの経験

  • Node.js(バージョン18.16.1を推奨)

  • 最新のWebブラウザー

Node.jsアプリケーションを作成する

アプリケーションを作成するには、まず新しいディレクトリを作成します。次に、そのディレクトリで以下のコマンドを実行して、新しいNode.jsアプリを初期化します。

npm init -y

続いて、以下のコマンドを実行して、このプロジェクトの依存関係をインストールします。

npm i express cors

最後に、JavaScriptファイルを作成し、index.jsという名前を付けます。以下のコードを貼り付けてください。

// Import required modules
const express = require("express");
const cors = require("cors");

// Initialize the Express application
const app = express();
app.use(express.json());

//Sample object that will serve as a database to store data
let sampleBooksData = {
  books: [
    {
      id: 1,
      title: "Harry Potter",
    },
    {
      id: 2,
      title: "The Da Vinci Code",
    },
    {
      id: 3,
      title: "Twilight",
    },
  ],
};

app.get("/books", cors(), (req, res, next) => {

  try {
    res.status(200).json(sampleBooksData);
  } catch (error) {
    console.error(`Error while reading from DB ${error}`);
    res.status(500).json({ message: "Internal Server Error" });
  }

});

// Start server on port defined by the variable PORT
let port = 3000;
app.listen(port, () =>
  console.log(`Server running at http://localhost:${port}`)
);

Express.jsでは、アプリ内のリクエストとレスポンスを処理するミドルウェア関数が利用できます。これらの関数は、受信リクエストや送信レスポンスを中継して変更できるため、ログ記録、認証、この例のCORS設定などの機能を実現できます。上の例では、const cors = require("cors");を使ってCORSモジュールを読み込みます。次のコードを使って、このモジュールを/booksルートにミドルウェア関数として適用します。

app.get("/books", cors(), (req, res, next) => {
...
} 

ただし、現状では/booksルートのcorsミドルウェアで、どのオリジンからGETリクエストを許可するのかを明示していません。そのため、この設定ではすべてのオリジンからこのパスへのリクエストが可能になります。セキュリティ上、この方法は推奨されませんが、本番環境向けに、より制限の厳しいルールを設定できます。

CORSを設定する

4種類の設定を使って、corsミドルウェアでCORSをより安全に実装できます。

特定のオリジンに対してCORSを有効にする

CORS設定のoriginオプションを使って、許可するオリジンを設定できます。信頼できる送信元だけにアクセスを制限することで、セキュリティが向上し、不正なクロスオリジンリクエストを防げます。以下の例では、`https://example.com`からのリクエストのみを許可します。

const corsOptions = {
  origin: 'https://example.com'
};
app.get('/books', cors(corsOptions), (req,res) => { /* ... */ });

許可するHTTPメソッドを設定する

methodsオプションを使って、許可するメソッドを定義することもできます。APIエンドポイントで必要な操作だけを許可することで、攻撃対象となる経路を減らせます。以下の例では、GETとPOSTのみを許可します。

corsOptions.methods = ['GET', 'POST'];

カスタムヘッダーと公開するヘッダーを設定する

また、allowedHeadersオプションでCORSヘッダーAccess-Control-Allow-Headersを設定し、exposedHeadersオプションでAccess-Control-Expose-Headersを設定できます。この方法なら、クライアントとサーバー間の柔軟な通信を維持しながら、機密データを適切に扱えます。以下の例では、Content-TypeとAuthorizationヘッダーのみを許可し、X-Custom-Headerのみを公開します。

corsOptions.allowedHeaders = ['Content-Type', 'Authorization'];
corsOptions.exposedHeaders = ['X-Custom-Header'];

プリフライトリクエストとキャッシュを設定する

最後に、maxAge設定でキャッシュ期間を指定し、プリフライトリクエスト(OPTIONS)のキャッシュを有効にできます。これにより、クライアントは指定した期間、サーバーのCORSポリシー情報をキャッシュできます。以降のリクエストでは、サーバーから再取得せず、キャッシュ済みのCORSポリシーデータを使用できます。

適切なmaxAge値を設定すると、応答時間とネットワーク負荷を減らしてパフォーマンスを最適化しつつ、CORSポリシーを最新の状態に保てます。以下の例では、maxAgeを86400秒(24時間)に設定します。

// Cache duration in seconds.
// Set maxAge to a high value (e.g., 86400) for long-lived caches.
// Default is no-cache.
const DAY_IN_SECONDS=86400;
corsOptions.maxAge=DAY_IN_SECONDS;

オプションの設定ができたので、CORSを有効にする必要がある任意のルートで、コードをミドルウェアとして使用できます。

app.get('<route-of-your-choice>', cors(corsOptions), (req,res) => { /* ... */ });

CORSはAPIの保護に役立ちますが、サードパーティ製の依存関係であることに変わりはありません。プロジェクトに取り込むCORSやその他の依存関係に、既知の脆弱性がなく、安全であることを確認する必要があります。たとえばSnyk Open Sourceを使うと、開発中や継続的インテグレーション/継続的デリバリー(CI/CD)パイプラインで、パッケージのセキュリティ上の問題を確認できます。

CORSのセキュリティ上の影響とベストプラクティス

CORSを正しく設定しないと、先ほど説明したセキュリティ上の脆弱性を引き起こすおそれがあります。具体的には、意図しないオリジンへの機密データの漏えい、悪意あるWebサイトによる不正操作(CSRF攻撃)、さらにはクロスサイトスクリプティング(XSS)などの手法による攻撃者のSOP保護の回避が考えられます。

こうした事態を避けるため、CORS設定を実装する際は必ずベストプラクティスに従ってください。このセクションの後半で説明するように、CORS以外のセキュリティヘッダーを実装するなど、さらにセキュリティ対策を講じることもできます。

ベストプラクティス

許可するオリジンの制限、安全なCookieとトークンの使用、公開するヘッダーの制限といったベストプラクティスは、アプリケーションとユーザーを安全に保つうえで欠かせません。以下では、コードサンプルを使って具体的な方法を説明します。

許可リストを使って許可するオリジンを制限する

corsOptionsオブジェクトで、すべてを許可するワイルドカード(*)を使わないようにし、許可リストにオリジンを指定します。

const allowedOrigins = ['https://example.com', 'https://another-example.com'];
corsOptions.origin = (origin, callback) => {
  if (allowedOrigins.includes(origin)) {
    callback(null, true);
  } else {
    callback(new Error('Not allowed by CORS'));
  }
};

アプリケーションがCookieとトークンを安全に送信できるよう、HTTPSスキームを使用してください。また、Passport.jsなどの信頼できる認証ライブラリや、JSON Web Token(JWT)ソリューションを利用しましょう。

corsOptions.credentials = true;

app.use(passport.initialize());
passport.use(new JwtStrategy(/* ... */));
app.get('/books', cors(corsOptions), passport.authenticate('jwt', { session: false }), (req,res) => { /* ... */ });

このコードでは、credentials to trueに設定すると、クロスオリジンリクエストに認証情報を含められます。

公開するヘッダーとHTTPメソッドを制限する

必要なヘッダーのみを指定するには、exposedHeadersオプションを使用します。また、設定でmethodsオプションを使い、CORSを有効にするメソッドを制限してください。

// Exposed Headers.
corsOptions.exposedHeaders = ['Content-Type', 'Authorization'];

// Permitted HTTP Methods
corsOptions.methods=['GET'];

CORS以外のセキュリティヘッダーを実装する

CORS以外の追加のセキュリティヘッダーを実装すると、アプリケーションをさらに保護できます。HTTPセキュリティヘッダーとは、クライアントとサーバー間のHTTP通信におけるセキュリティ上の特性を示すHTTPヘッダーです。

Helmet.jsは、適切なデフォルト値を備えたセキュリティヘッダーを提供します。ただし、CORS設定で定義した機能を有効にするには、コンテンツセキュリティポリシー(CSP)の設定が必要です。以下の手順で設定してください。

まず、Node.jsアプリケーションのディレクトリでコマンドラインを開き、以下のコマンドを実行してHelmetをインストールします。

npm i helmet

次に、以下のコードを使ってパッケージをアプリケーションに読み込みます。

const helmet = require('helmet');
app.use(helmet());

続いて、以下のようにCSPをCORSの設定に合わせて設定します。

const cspConfig = {
  directives: {
    defaultSrc: ["'self'", 'https://example.com'],
    // ...other CSP directives matching your app's needs.
  }
};
app.use(helmet.contentSecurityPolicy(cspConfig));

最後に、設定を確認しましょう。特にアプリケーションが大きくなるにつれて、設定の見落としや入力ミスによってセキュリティが損なわれることがあります。Snyk Codeと、Visual Studio Code拡張機能などのSnyk開発者向けツール拡張機能は、コードベースにおけるセキュリティヘッダーの不足や設定ミスを検出します。こうしたサードパーティツールを活用し、開発中にセキュリティ上の問題へ先回りして対処することで、安全なNode.jsアプリケーションを維持できます。

CORSの実装とセキュリティをテストする

潜在的な脅威からアプリケーションを確実に保護するには、CORSの実装とセキュリティをテストしましょう。これにより、潜在的な脆弱性や設定ミスを特定できます。また、CORSのルールが意図したとおりに機能し、データ漏えいやサードパーティのWebサイトからの不正アクセスなど、セキュリティリスクにつながっていないことも確認できます。

CORSの実装とセキュリティをテストする方法はいくつかあります。以下では、一般的な方法をいくつか紹介します。

ブラウザーの開発者ツール

最新のWebブラウザーには、Webアプリケーションを調査してデバッグするための開発者ツールが用意されています。ネットワークリクエストとレスポンスを確認すれば、これらのツールでCORSをテストできます。クロスオリジンリクエストをシミュレートして、CORSルールが適切に許可またはブロックすることを確認しましょう。

後ほど、先ほど作成したNode.jsアプリケーションを使って、この方法をさらに詳しく説明します。

Postman

人気のAPI開発・テストツールPostmanを使って、CORSをテストできます。HTTPリクエストを作成し、カスタムヘッダーを指定してCORSの設定をテストできます。また、Postmanではプリフライトリクエストを自動生成して、プリフライトリクエストのキャッシュをテストすることもできます。Postmanのスクリプト機能を使えば、テストシナリオも自動化できます。

カスタムスクリプト

カスタムスクリプトを作成して、CORSの設定をテストできます。たとえば、PythonやJavaScriptなどのスクリプト言語を使ってHTTPリクエストを送信し、レスポンスにCORSヘッダーが含まれていることを確認します。また、クロスオリジンリクエストをシミュレートし、CORSルールによってリクエストが適切にブロックまたは許可されることを確認できます。

ブラウザーの開発者ツールを使ってCORS設定をテストする方法

ブラウザーの開発者ツールを使って、サンプルのNode.jsアプリケーションのCORS設定をテストできます。以下の手順に従ってください。

まず、Node.jsアプリケーションを起動します。アプリケーションのディレクトリでコマンドラインを開き、以下のコマンドを実行します。

node index.js

次に、任意のウェブブラウザーでGoogleのホームページにアクセスします。

検索バー、検索ボタン、ハウサ語、イボ語、ヨルバ語、ナイジェリア・ピジン語などの言語オプションが表示された、ダークモードのGoogleホームページ

次に、開発者コンソールを開きます。Chromiumベースのブラウザーをお使いの場合は、macOSではOption+⌘+J、Windows/LinuxではShift+CTRL+ Jを押します。または、お使いのブラウザーに対応するコマンドを使用してください。

以下のコードを貼り付けて、Return/Enterキーを押します。

fetch('http://127.0.0.1:3000/books')
.then((response) => response.json())
.then((json) => console.log(json))

サンプルアプリケーションの現在のCORS設定では、CORSポリシーによってオリジン(Google)からサーバー上の/booksリソースへのアクセスがブロックされたことを示すエラーと、内部サーバーエラーが表示される可能性が高いでしょう。

CORSポリシーによってブロックされたfetchリクエストと、500 Internal Server Errorを表示するChrome DevToolsのコンソール

アクセスがブロックされたのは、オリジンhttps://www.google.comが、SOPを回避できる許可済みオリジンに含まれていないためです。corsOptionsオブジェクトのcorsミドルウェアに渡す`origin`オプションの設定では、このオリジンへのサーバーからのレスポンスが許可されていません。ブラウザーによって表示やエラーメッセージは異なる場合がありますが、いずれの場合もリクエストがブロックされたことを示すエラーが表示されます。

このエラーは、CORSの実装ルールが意図したとおりに機能していることを示しています。許可されていないオリジンからアプリケーションにアクセスすることはできません。

https://www.google.comからのリクエストを許可するには、corsOptionsオブジェクトのoriginオプションに指定します。

const allowedOrigins = ['https://example.com', 'https://another-example.com', 'https://www.google.com'];
corsOptions.origin = (origin, callback) => {
  if (allowedOrigins.includes(origin)) {
    callback(null, true);
  } else {
    callback(new Error('Not allowed by CORS'));
  }
};

app.get('books/', cor(corsOptions), (req,res) => { /* ... */ })

サーバーを再起動し、開発者コンソールでfetchコマンドを再実行すると、想定どおりに動作するはずです。それでもエラーが表示される場合は、別のブラウザーでスクリプトを実行してみてください。Braveブラウザーでfetchコマンドを動作させるには、Shieldsを無効にする必要があります。なお、現在のCORS設定ではSafariブラウザーで問題が発生する場合があります。これは、Safariが同一オリジンポリシー(SOP)を厳格に適用し、異なるオリジンからのリクエストを制限するためです。

Googleのホームページと、3冊の本の配列を返すfetchリクエストが表示されたChrome DevToolsのコンソールを並べた画面。

CORS設定でhttps://www.google.comを許可したため、エラーではなく書籍の一覧が表示されます。

次のステップ

CORSを導入するとアプリケーションに便利な機能を追加できますが、設定を誤るとアプリがセキュリティ上の脆弱性にさらされるおそれがあります。こうしたリスクを踏まえ、可能な限りベストプラクティスを適用し、Node.jsアプリケーションを潜在的な脅威から保護しましょう。

安全なクロスオリジン通信を実現するため、corsミドルウェアでは特定のオリジン、HTTPメソッド、ヘッダー、プリフライトリクエストを設定できます。Snyk Codeやブラウザーの開発者ツールなどを活用すれば、開発中にエラーや脆弱性を先回りして見つけることもできます。このアプローチにより、本番環境へのリリース時にアプリケーションの安全性を確保し、システムとユーザーを保護できます。

これらのベストプラクティスに従ってCORS設定を行ったら、Snyk Codeをお試しください。アプリケーションをリアルタイムでテストし、セキュリティ設定を再確認できます。月に200回まで無料でテストできます。

Capture the Flagを始めよう

オンデマンドのバーチャル入門ワークショップを見て、Capture the Flagの課題の解き方を学びましょう。

続きを読む

Blog

フロンティアモデルは脆弱性を発見した。攻撃者だけがエクスプロイトチェーンを見つけた。

静的解析で欠陥は見つかりましたが、ライブ攻撃テストで侵害につながる連鎖を実証できたのは唯一でした。Evo COS、Claude Security、Claude Code Securityを比較します。

feature insights context
Blog

自律型攻撃はすでに始まっている。防御もそのスピードに追いつかなければならない。

自律型攻撃者によって、防御に使える時間は短くなっています。継続的な検出、修復、検証、予防で、セキュリティチームが攻撃に歩調を合わせる方法をご紹介します。

Blog

AIコーディングエージェントが不適切なアクセス制御を繰り返し実装する理由

AIコーディングエージェントは、コンパイルが通りレビューも通過する一方で、あるテナントのデータを別のテナントに公開してしまう認可ロジックを生成することがあります。不適切なアクセス制御が検出しにくい理由と、その防止策をご紹介します。