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

目的

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

ドキュメントの内容

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

脅威リスト

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

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

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

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

const  = ('node:net');

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

.(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 ブラウザで開かれたウェブサイトは WebSocket および HTTP リクエストを送信できるため、ローカルで実行されているデバッグインスペクターを標的にすることができます。これは通常、最新のブラウザに実装されている同一オリジンポリシーによって防止されます。このポリシーは、スクリプトが異なるオリジンからのリソースに到達することを禁止します (つまり、悪意のあるウェブサイトはローカル IP アドレスから要求されたデータを読み取ることができません)。

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

緩和策

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

不正なアクターへの機密情報の漏洩 (CWE-552)

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

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

緩和策

  • npm publish --dry-run を使用して、公開するすべてのファイルをリストします。パッケージを公開する前に内容を確認してください。
  • .gitignore.npmignore などの ignore ファイルを作成し、維持することも重要です。これらのファイルを通じて、公開すべきでないファイル/フォルダーを指定できます。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 のためです)。暗号アプリケーションには、組み込みの暗号 API または WebAssembly (ネイティブに実装されていないアルゴリズムの場合) を使用してください。

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

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

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

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

サプライチェーン攻撃

Node.js アプリケーションへのサプライチェーン攻撃は、その依存関係 (直接または推移的) のいずれかが侵害されたときに発生します。これは、アプリケーションが依存関係の仕様に甘すぎる (不要な更新を許容する) こと、および/または仕様の一般的なタイプミス (タイポスクワッティングに対して脆弱) が原因で発生する可能性があります。

上流のパッケージを制御した攻撃者は、悪意のあるコードを含む新しいバージョンを公開できます。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 ドキュメント を参照してください。

緩和策

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

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

モンキーパッチとは、既存の動作を変更することを目的として、実行時にプロパティを変更することを指します。例

.. = function () {
  // overriding the global [].push
};

緩和策

--frozen-intrinsics フラグは、実験的な¹ frozen intrinsics を有効にします。これは、すべての組み込み JavaScript オブジェクトと関数が再帰的に凍結されることを意味します。したがって、以下のスニペットは Array.prototype.push のデフォルトの動作を上書きしません

.. = function () {
  // 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  = { : 1, : 2 };
const  = .('{"__proto__": { "polluted": true}}');

const  = .({}, , );
.(.polluted); // true

// Potential DoS
const  = .('{"__proto__": null}');
const  = .(, );
.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 Best Practices Badge Program プロジェクトは、各ベストプラクティスにどのように準拠しているかを説明することで、自主的に自己認証できます。これにより、プロジェクトに追加できるバッジが生成されます。