セキュリティのベストプラクティス

意図

このドキュメントは、現在の 脅威モデル を拡張し、Node.js アプリケーションを保護する方法に関する広範なガイドラインを提供することを目的としています。

ドキュメントの内容

  • ベストプラクティス:ベストプラクティスを簡略化してまとめたものです。 この issue または このガイドライン を出発点として使用できます。このドキュメントは Node.js に特化したものであることに注意してください。広範なものを探している場合は、OSSF ベストプラクティス を検討してください。
  • 攻撃の説明:脅威モデルで言及している攻撃を、平易な英語で、いくつかのコード例(可能な場合)を交えて説明し、文書化します。
  • サードパーティライブラリ:脅威(タイポスクワッティング攻撃、悪意のあるパッケージなど)と、Node モジュールの依存関係などに関するベストプラクティスを定義します。

脅威リスト

HTTPサーバーのサービス拒否(CWE-400)

これは、アプリケーションが受信した HTTP リクエストの処理方法のために、設計された目的で使用できなくなる攻撃です。これらのリクエストは、悪意のあるアクターによって意図的に作成される必要はありません。設定ミスやバグのあるクライアントも、サービス拒否を引き起こすリクエストパターンをサーバーに送信する可能性があります。

HTTP リクエストは Node.js HTTP サーバーによって受信され、登録されたリクエストハンドラーを介してアプリケーションコードに渡されます。サーバーはリクエストボディの内容を解析しません。したがって、リクエストハンドラーに渡された後のボディの内容によって引き起こされる DoS は、Node.js 自体の脆弱性ではありません。アプリケーションコードが正しく処理する責任があるからです。

Webサーバーがソケットエラーを適切に処理することを確認してください。たとえば、エラーハンドラーなしでサーバーが作成された場合、DoS に対して脆弱になります。

const net = require('node:net');

const server = net.createServer(function (socket) {
  // socket.on('error', console.error) // this prevents the server to crash
  socket.write('Echo server\r\n');
  socket.pipe(socket);
});

server.listen(5000, '0.0.0.0');

不正なリクエスト が実行されると、サーバーがクラッシュする可能性があります。

リクエストの内容によって引き起こされない DoS 攻撃の例は、Slowloris です。この攻撃では、HTTP リクエストがゆっくりと断片化され、一度に 1 つの断片が送信されます。完全なリクエストが配信されるまで、サーバーは進行中のリクエスト専用のリソースを保持します。これらのリクエストが同時に大量に送信されると、同時接続数がすぐに最大に達し、サービス拒否が発生します。このように、攻撃はリクエストの内容ではなく、サーバーに送信されるリクエストのタイミングとパターンに依存します。

緩和策

  • リバースプロキシを使用して、リクエストを受信して Node.js アプリケーションに転送します。リバースプロキシは、キャッシング、ロードバランシング、IP ブラックリストなどを提供でき、DoS 攻撃が効果的になる可能性を減らします。
  • サーバーのタイムアウトを正しく設定して、アイドル状態の接続や、リクエストの到着が遅すぎる接続をドロップできるようにします。http.Server のさまざまなタイムアウト、特に headersTimeoutrequestTimeouttimeout、および keepAliveTimeout を参照してください。
  • ホストごとおよび合計で、開いているソケットの数を制限します。http ドキュメント、特に agent.maxSocketsagent.maxTotalSocketsagent.maxFreeSockets、および server.maxRequestsPerSocket を参照してください。

DNS リバインディング(CWE-346)

これは、--inspect スイッチを使用してデバッグインスペクターが有効になっている状態で実行されている Node.js アプリケーションをターゲットにする可能性のある攻撃です。

Web ブラウザーで開かれた Web サイトは WebSocket および HTTP リクエストを行うことができるため、ローカルで実行されているデバッグインスペクターをターゲットにできます。これは通常、最新のブラウザーによって実装されている 同一生成元ポリシー によって防止されています。このポリシーでは、スクリプトが異なる生成元からのリソースに到達することを禁止しています(つまり、悪意のある Web サイトがローカル IP アドレスから要求されたデータを読み取ることができないことを意味します)。

ただし、DNS リバインディングを通じて、攻撃者はリクエストの生成元を一時的に制御して、ローカル IP アドレスから発信されたように見せかけることができます。これは、Web サイトと、その IP アドレスの解決に使用される DNS サーバーの両方を制御することによって行われます。詳細については、DNS リバインディング Wiki を参照してください。

緩和策

  • SIGUSR1シグナルに対するインスペクターを無効にするには、process.on('SIGUSR1', …)リスナーをアタッチします。
  • 本番環境ではインスペクタープロトコルを実行しないでください。

許可されていないアクターへの機密情報の暴露 (CWE-552)

現在のディレクトリに含まれるすべてのファイルとフォルダーは、パッケージの公開中に npm レジストリにプッシュされます。

この動作を制御するメカニズムがいくつかあります。.npmignore.gitignore でブロックリストを定義するか、package.json で許可リストを定義します。

緩和策

  • npm publish --dry-run を使用して、公開するすべてのファイルを一覧表示します。パッケージを公開する前に、内容を必ず確認してください。
  • また、.gitignore.npmignore などの無視ファイルを作成および維持することも重要です。これらのファイル全体で、公開すべきではないファイル/フォルダーを指定できます。package.jsonfiles プロパティでは、逆の操作(許可リスト)が可能です。
  • 情報が漏洩した場合は、必ず パッケージの公開を取り消してください。

HTTP リクエストスマグリング (CWE-444)

これは、2つのHTTPサーバー(通常はプロキシとNode.jsアプリケーション)が関与する攻撃です。クライアントは、フロントエンドサーバー(プロキシ)を最初に通過し、次にバックエンドサーバー(アプリケーション)にリダイレクトされるHTTPリクエストを送信します。フロントエンドとバックエンドが曖昧なHTTPリクエストを異なる方法で解釈する場合、攻撃者がフロントエンドには認識されず、バックエンドには認識される悪意のあるメッセージを送信し、事実上プロキシサーバーを「密輸」する可能性があります。

詳細な説明と例については、CWE-444を参照してください。

この攻撃は、Node.jsが(任意の)HTTPサーバーとは異なる方法でHTTPリクエストを解釈することに依存するため、攻撃が成功するのは、Node.js、フロントエンドサーバー、またはその両方に脆弱性があることが原因である可能性があります。Node.jsによるリクエストの解釈方法がHTTP仕様(RFC7230を参照)と一致している場合、Node.jsの脆弱性とはみなされません。

緩和策

  • HTTPサーバーを作成するときに、insecureHTTPParserオプションを使用しないでください。
  • 曖昧なリクエストを正規化するようにフロントエンドサーバーを構成してください。
  • Node.jsと選択したフロントエンドサーバーの両方で、新しいHTTPリクエストスマグリングの脆弱性を継続的に監視してください。
  • 可能な場合は、エンドツーエンドでHTTP/2を使用し、HTTPのダウングレードを無効にしてください。

タイミング攻撃による情報漏洩 (CWE-208)

これは、攻撃者がリクエストに対するアプリケーションの応答にかかる時間を測定するなどして、潜在的に機密性の高い情報を学習できる攻撃です。この攻撃はNode.jsに固有のものではなく、ほぼすべてのランタイムをターゲットにすることができます。

アプリケーションがタイミングの影響を受けやすい操作(例:分岐)でシークレットを使用する場合、攻撃は可能です。一般的なアプリケーションでの認証処理について考えてみましょう。ここで、基本的な認証方法には、資格情報としてメールアドレスとパスワードが含まれます。ユーザー情報は、理想的にはDBMSから、ユーザーが入力した情報から取得されます。ユーザー情報を取得すると、パスワードがデータベースから取得したユーザー情報と比較されます。組み込みの文字列比較を使用すると、同じ長さの値の場合、時間が長くなります。この比較を許容できる回数実行すると、意図せずにリクエストの応答時間が増加します。リクエストの応答時間を比較することで、攻撃者は大量のリクエストでパスワードの長さと値を推測できます。

緩和策

  • crypto APIは、定数時間アルゴリズムを使用して、実際の機密値と予期される機密値を比較する関数timingSafeEqualを公開します。

  • パスワードの比較には、ネイティブのcryptoモジュールでも使用できるscryptを使用できます。

  • より一般的には、可変時間操作でシークレットを使用することは避けてください。これには、シークレットの分岐や、攻撃者が同じインフラストラクチャ(例:同じクラウドマシン)に配置されている可能性がある場合、メモリへのインデックスとしてシークレットを使用することも含まれます。JavaScriptで定数時間コードを作成するのは困難です(一部にはJITが原因です)。暗号化アプリケーションの場合は、組み込みのcrypto APIまたはWebAssembly(ネイティブで実装されていないアルゴリズムの場合)を使用してください。

悪意のあるサードパーティモジュール (CWE-1357)

現在、Node.jsでは、どのパッケージもネットワークアクセスなどの強力なリソースにアクセスできます。さらに、ファイルシステムへのアクセスも可能であるため、任意のデータを任意の場所に送信できます。

Nodeプロセスで実行されているすべてのコードは、eval()(または同等のもの)を使用して追加の任意のコードをロードおよび実行できます。ファイルシステムへの書き込みアクセス権を持つすべてのコードは、ロードされる新規または既存のファイルに書き込むことで同じことを実現できます。

Node.jsには、ロードされたリソースを信頼できないものまたは信頼できるものとして宣言する、試験的な¹ ポリシーメカニズムがあります。ただし、このポリシーはデフォルトでは有効になっていません。依存関係のバージョンを固定し、一般的なワークフローまたはnpmスクリプトを使用して脆弱性の自動チェックを実行してください。パッケージをインストールする前に、このパッケージが維持されており、予期されるすべてのコンテンツが含まれていることを確認してください。GitHubのソースコードは公開されたものと同じではない場合があるため、node_modulesで検証してください。

サプライチェーン攻撃

Node.jsアプリケーションに対するサプライチェーン攻撃は、その依存関係(直接的または推移的)の1つが侵害された場合に発生します。これは、依存関係の指定があまりにも緩い(不要な更新を許可する)ことや、指定の一般的なタイプミス(タイプスクワッティングに対して脆弱)が原因で発生する可能性があります。

アップストリームパッケージを制御する攻撃者は、悪意のあるコードを含む新しいバージョンを公開できます。Node.jsアプリケーションが、安全に使用できるバージョンを厳密に指定せずにそのパッケージに依存している場合、パッケージは最新の悪意のあるバージョンに自動的に更新され、アプリケーションが侵害される可能性があります。

package.jsonファイルで指定された依存関係は、正確なバージョン番号または範囲を持つことができます。ただし、依存関係を正確なバージョンに固定する場合、その推移的な依存関係は固定されません。これにより、アプリケーションは不要/予期しない更新に対して脆弱なままになります。

考えられる攻撃ベクトル

  • タイプスクワッティング攻撃
  • ロックファイルポイズニング
  • 侵害されたメンテナー
  • 悪意のあるパッケージ
  • 依存関係の混乱

緩和策

  • --ignore-scriptsでnpmが任意のスクリプトを実行するのを防ぎます。
    • さらに、npm config set ignore-scripts trueでグローバルに無効にできます。
  • 依存関係のバージョンを、範囲または変更可能なソースからのバージョンではなく、特定のイミュータブルバージョンに固定します。
  • すべての依存関係(直接的および推移的)を固定するロックファイルを使用します。
  • [npm-audit][]などのツールを使用して、CIで新しい脆弱性のチェックを自動化します。
    • Socketなどのツールを使用して、静的解析でパッケージを分析し、ネットワークやファイルシステムへのアクセスなどの危険な動作を見つけることができます。
  • npm installの代わりにnpm ciを使用します。これにより、ロックファイルが強制されるため、ロックファイルとpackage.jsonファイル間の不一致が発生するとエラーが発生します(package.jsonを優先してロックファイルを暗黙的に無視する代わりに)。
  • 依存関係の名前にエラー/タイプミスがないか、package.jsonファイルを注意深く確認してください。

メモリアクセス違反 (CWE-284)

メモリベースまたはヒープベースの攻撃は、メモリ管理エラーと悪用可能なメモリアロケータの組み合わせに依存します。すべてのランタイムと同様に、Node.jsは、プロジェクトが共有マシンで実行されている場合にこれらの攻撃に対して脆弱です。セキュアヒープを使用すると、ポインターのオーバーランとアンダーランによる機密情報の漏洩を防ぐのに役立ちます。

残念ながら、セキュアヒープはWindowsでは利用できません。詳細については、Node.jsのセキュアヒープドキュメントを参照してください。

緩和策

  • アプリケーションに応じて--secure-heap=nを使用します。ここで、nは割り当てられた最大バイトサイズです。
  • 共有マシンで本番アプリを実行しないでください。

モンキーパッチ (CWE-349)

モンキーパッチとは、既存の動作を変更することを目的として、ランタイムでプロパティを修正することを指します。例

// eslint-disable-next-line no-extend-native
Array.prototype.push = function (item) {
  // overriding the global [].push
};

緩和策

--frozen-intrinsicsフラグは、試験的な¹フローズンイントリンシックスを有効にします。これは、組み込みのすべてのJavaScriptオブジェクトと関数が再帰的にフリーズされることを意味します。したがって、次のスニペットは、Array.prototype.pushのデフォルトの動作をオーバーライドしません

// eslint-disable-next-line no-extend-native
Array.prototype.push = function (item) {
  // overriding the global [].push
};

// Uncaught:
// TypeError <Object <Object <[Object: null prototype] {}>>>:
// Cannot assign to read only property 'push' of object ''

ただし、globalThisを使用して新しいグローバルを定義したり、既存のグローバルを置き換えたりすることは引き続き可能です。

> globalThis.foo = 3; foo; // you can still define new globals
3
> globalThis.Array = 4; Array; // However, you can also replace existing globals
4

したがって、Object.freeze(globalThis)を使用すると、グローバルが置き換えられないことが保証されます。

プロトタイプ汚染攻撃 (CWE-1321)

プロトタイプ汚染とは、__proto__、_constructor_、_prototype_、および組み込みプロトタイプから継承されたその他のプロパティの使用を悪用して、JavaScript言語項目にプロパティを変更または挿入できる可能性を指します。

const a = { a: 1, b: 2 };
const data = JSON.parse('{"__proto__": { "polluted": true}}');

const c = Object.assign({}, a, data);
console.log(c.polluted); // true

// Potential DoS
const data2 = JSON.parse('{"__proto__": null}');
const d = Object.assign(a, data2);
d.hasOwnProperty('b'); // Uncaught TypeError: d.hasOwnProperty is not a function

これは、JavaScript言語から継承された潜在的な脆弱性です。

:

緩和策

  • 安全でない再帰的マージを避けてください。 CVE-2018-16487を参照してください。
  • 外部/信頼されていないリクエストに対してJSONスキーマ検証を実装します。
  • Object.create(null)を使用して、プロトタイプなしでオブジェクトを作成します。
  • プロトタイプのフリーズ:Object.freeze(MyObject.prototype)
  • --disable-protoフラグを使用して、Object.prototype.__proto__プロパティを無効にします。
  • Object.hasOwn(obj, keyFromObj)を使用して、プロトタイプからではなく、オブジェクトにプロパティが直接存在することを確認します。
  • Object.prototypeのメソッドを使用することは避けてください。

制御されていない検索パス要素 (CWE-427)

Node.jsは、モジュール解決アルゴリズムに従ってモジュールをロードします。したがって、モジュールが要求された(require)ディレクトリは信頼されていると想定します。

つまり、次のアプリケーションの動作が想定されます。次のディレクトリ構造を想定します

  • app/
    • server.js
    • auth.js
    • auth

server.jsがrequire('./auth')を使用する場合、モジュール解決アルゴリズムに従って、auth.jsではなくauthをロードします。

緩和策

実験的な¹ 整合性チェック付きのポリシーメカニズムを使用すると、上記の脅威を回避できます。上記のディレクトリに対して、次のpolicy.jsonを使用できます。

{
  "resources": {
    "./app/auth.js": {
      "integrity": "sha256-iuGZ6SFVFpMuHUcJciQTIKpIyaQVigMZlvg9Lx66HV8="
    },
    "./app/server.js": {
      "dependencies": {
        "./auth": "./app/auth.js"
      },
      "integrity": "sha256-NPtLCQ0ntPPWgfVEgX46ryTNpdvTWdQPoZO3kHo0bKI="
    }
  }
}

したがって、authモジュールを要求する場合、システムは整合性を検証し、予期されるものと一致しない場合はエラーをスローします。

» node --experimental-policy=policy.json app/server.js
node:internal/policy/sri:65
      throw new ERR_SRI_PARSE(str, str[prevIndex], prevIndex);
      ^

SyntaxError [ERR_SRI_PARSE]: Subresource Integrity string "sha256-iuGZ6SFVFpMuHUcJciQTIKpIyaQVigMZlvg9Lx66HV8=%" had an unexpected "%" at position 51
    at new NodeError (node:internal/errors:393:5)
    at Object.parse (node:internal/policy/sri:65:13)
    at processEntry (node:internal/policy/manifest:581:38)
    at Manifest.assertIntegrity (node:internal/policy/manifest:588:32)
    at Module._compile (node:internal/modules/cjs/loader:1119:21)
    at Module._extensions..js (node:internal/modules/cjs/loader:1213:10)
    at Module.load (node:internal/modules/cjs/loader:1037:32)
    at Module._load (node:internal/modules/cjs/loader:878:12)
    at Module.require (node:internal/modules/cjs/loader:1061:19)
    at require (node:internal/modules/cjs/helpers:99:18) {
  code: 'ERR_SRI_PARSE'
}

ポリシーの変更を避けるため、常に--policy-integrityを使用することを推奨します。

本番環境での実験的機能

本番環境での実験的機能の使用は推奨されません。実験的な機能は、必要に応じて破壊的な変更を受ける可能性があり、その機能は安全に安定していません。ただし、フィードバックは非常にありがたいです。

OpenSSFツール

OpenSSFは、特にnpmパッケージを公開する予定の場合に非常に役立つ可能性のあるいくつかのイニシアチブを主導しています。これらのイニシアチブには以下が含まれます。

  • OpenSSF Scorecard Scorecardは、一連の自動セキュリティリスクチェックを使用してオープンソースプロジェクトを評価します。これを使用して、コードベースの脆弱性や依存関係を事前に評価し、脆弱性の受け入れについて情報に基づいた意思決定を行うことができます。
  • OpenSSFベストプラクティスバッジプログラム プロジェクトは、各ベストプラクティスをどのように遵守しているかを説明することにより、自主的に自己認証できます。これにより、プロジェクトに追加できるバッジが生成されます。