セキュリティベストプラクティス
目的
このドキュメントは、現在の脅威モデルを拡張し、Node.js アプリケーションを保護する方法に関する包括的なガイドラインを提供することを目的としています。
ドキュメント内容
- ベストプラクティス:ベストプラクティスの簡素化された要約。この issueやこのガイドラインを出発点として使用できます。このドキュメントは Node.js に固有のものであることに注意することが重要です。より広範な情報を探している場合は、OSSF ベストプラクティスを参照してください。
- 攻撃の説明:脅威モデルで言及している攻撃を、平易な英語とコード例(可能な場合)を使用して説明し、文書化します。
- サードパーティライブラリ:脅威(タイポスクワッティング攻撃、悪意のあるパッケージなど)と、ノードモジュールの依存関係に関するベストプラクティスなどを定義します。
脅威リスト
HTTP サーバーのサービス拒否(CWE-400)
これは、アプリケーションが受信する HTTP リクエストの処理方法のために、設計された目的で利用できなくなる攻撃です。これらのリクエストは、悪意のある攻撃者によって故意に作成される必要はありません。誤って構成されたり、バグのあるクライアントも、サービス拒否につながるリクエストのパターンをサーバーに送信する可能性があります。
HTTP リクエストは Node.js HTTP サーバーによって受信され、登録されたリクエストハンドラを介してアプリケーションコードに渡されます。サーバーはリクエスト本文の内容を解析しません。したがって、リクエストハンドラに渡された後の本文の内容によって発生する DoS は、Node.js 自体の脆弱性ではありません。これは、正しく処理することはアプリケーションコードの責任だからです。
Web サーバーがソケットエラーを適切に処理するようにします。たとえば、エラーハンドラなしでサーバーが作成されると、DoS に対して脆弱になります。
import net from 'node:net'
const server = net.createServer(socket => {
// socket.on('error', console.error) // これにより、サーバーがクラッシュするのを防ぎます
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
のさまざまなタイムアウト、特にheadersTimeout
、requestTimeout
、timeout
、keepAliveTimeout
を参照してください。 - ホストごと、および合計のオープンソケット数を制限します。http ドキュメント 、特に
agent.maxSockets
、agent.maxTotalSockets
、agent.maxFreeSockets
、server.maxRequestsPerSocket
を参照してください。
DNS リバインディング (CWE-346)
これは、 --inspect スイッチを使用してデバッグインスペクターを有効にして実行されている Node.js アプリケーションをターゲットにする可能性のある攻撃です。
Web ブラウザで開かれた Web サイトは WebSocket と HTTP リクエストを行うことができるため、ローカルで実行されているデバッグインスペクターをターゲットにすることができます。これは通常、最新のブラウザによって実装された同オリジンポリシーによって防止されます。同オリジンポリシーは、異なるオリジンからのリソースにスクリプトがアクセスすることを禁止します(つまり、悪意のある Web サイトがローカル IP アドレスから要求されたデータを読み取ることはできません)。
ただし、DNS リバインディングにより、攻撃者はリクエストのオリジンを一時的に制御して、ローカル IP アドレスから発信されたように見せることができます。これは、Web サイトと、その IP アドレスを解決するために使用される DNS サーバーの両方を制御することによって行われます。詳細については、DNS リバインディングの wikiを参照してください。
対策
process.on(‘SIGUSR1’, …)
リスナーをアタッチして、SIGUSR1 シグナルでインスペクターを無効にします。- 本番環境ではインスペクタープロトコルを実行しないでください。
権限のないアクタへの機密情報の公開 (CWE-552)
現在のディレクトリに含まれるすべてのファイルとフォルダは、パッケージ公開中に npm レジストリにプッシュされます。
.npmignore
と.gitignore
でブロックリストを定義するか、package.json
で許可リストを定義することにより、この動作を制御するいくつかのメカニズムがあります。
対策
npm publish --dry-run
を使用して、公開されるすべてのファイルをリストします。パッケージを公開する前に、内容を確認してください。.gitignore
や.npmignore
などの無視ファイルを生成して維持することも重要です。これらのファイルで、公開しないファイル/フォルダを指定できます。package.json
の files プロパティ は、逆の操作である「許可」リストを可能にします。- 公開された場合、パッケージの公開停止 を行います。
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 では、どのパッケージもネットワークアクセスなどの強力なリソースにアクセスできます。さらに、ファイルシステムにもアクセスできるため、あらゆるデータをどこにでも送信できます。
ノードプロセスで実行されているすべてのコードは、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)
モンキーパッチングとは、既存の動作を変更することを目的として、ランタイムでプロパティを変更することを指します。例:
// eslint-disable-next-line no-extend-native
Array.prototype.push = function (item) {
// グローバルの[].pushをオーバーライド
}
対策
--frozen-intrinsics
フラグにより、実験的な ¹ 凍結された組み込み関数が有効になります。これは、すべての組み込み JavaScript オブジェクトと関数が再帰的に凍結されることを意味します。そのため、次のスニペットではArray.prototype.push
のデフォルトの動作はオーバーライドされません。
// eslint-disable-next-line no-extend-native
Array.prototype.push = function (item) {
// グローバルの[].pushをオーバーライド
}
// Uncaught:
// TypeError <Object <Object <[Object: null prototype] {}>>>:
// Cannot assign to read only property 'push' of object '
ただし、globalThis
を使用して新しいグローバル変数を定義し、既存のグローバル変数を置き換えることは依然として可能です。
globalThis.foo = 3; foo; // 新しいグローバル変数を定義できます 3
globalThis.Array = 4; Array; // 既存のグローバル変数を置き換えることもできます 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
// 潜在的な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-2022-21824 (Node.js)
- CVE-2018-3721 (サードパーティライブラリ: Lodash)
対策
- 安全でない再帰的なマージを避ける、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 ベストプラクティスバッジプログラム プロジェクトは、各ベストプラクティスへの準拠方法を記述することで、自主的に自己認証できます。これにより、プロジェクトに追加できるバッジが生成されます。