Aikido

グローバル変数がNode.jsサーバーのデータリークを引き起こす理由

セキュリティ

ルール
避ける 意図しない グローバル 変数 キャッシュ Node.js
および Python サーバでは グローバル変数 変数 持続 リクエストを越えて
リクエストにまたがって持続します、 そのため データ リーク そして レース を引き起こします。

対応言語 JavaScript、 TypeScript、 Python

はじめに

Node.jsサーバーのグローバル変数は、単一のリクエストだけでなく、プロセスのライフタイムにわたって持続します。リクエスト・ハンドラがユーザ・データをグローバル変数に格納すると、そのデータは異なるユーザからの後続のリクエストでもアクセス可能なままです。これは、ユーザ A のセッションデータ、認証トークン、または個人情報がユーザ B に漏れるというセキュリティ上の脆弱性を生み出します。

なぜそれが重要なのか

セキュリティへの影響(データリーク):ユーザー固有のデータをキャッシュするグローバル変数は、リクエスト間のデータリークを引き起こします。あるユーザーの認証状態、セッションデータ、または個人情報が他のユーザーから見えるようになり、プライバシーとセキュリティの境界を侵害します。

レースコンディション:複数の同時リクエストで同じグローバル変数が変更されると、予測できない動作が発生する可能性が高くなります。ユーザーAのデータがユーザーBのリクエストによって処理の途中で上書きされ、不正な計算や破損した状態になったり、ユーザーがお互いのデータを見たりする可能性があります。

デバッグの複雑さ:グローバル変数のキャッシュによって引き起こされる問題は、リクエストのタイミングと同時実行性に依存するため、再現が難しいことで有名です。バグは負荷のかかる本番環境では断続的に現れますが、シングルスレッドの開発テストではほとんど現れません。

メモリーリーク:クリーンアップされずにデータが蓄積されたグローバル変数は、時間の経過とともに無制限に増大する。リクエストのたびにグローバルキャッシュや配列にデータが追加され、最終的にはサーバーのメモリを使い果たし、プロセスの再起動が必要になります。

コード例

非準拠:

let currentUser = null;
let requestData = {};

app.get('/profile', async (req, res) => {
    currentUser = await getUserById(req.userId);
    requestData = req.body;

    const profile = await buildUserProfile(currentUser);
    res.json(profile);
});

function buildUserProfile(user) {
    return {
        name: currentUser.name,
        data: requestData
    };
}

なぜ間違っているのか: グローバル変数 currentUser と requestData は、リクエスト間で持続します。複数のリクエストが同時に実行されると、ユーザAのbuildUserProfile()が実行されている間にユーザBのリクエストがcurrentUserを上書きし、ユーザAにユーザBのデータが表示される可能性があります。

✅ 準拠:

app.get('/profile', async (req, res) => {
    const currentUser = await getUserById(req.userId);
    const requestData = req.body;

    const profile = buildUserProfile(currentUser, requestData);
    res.json(profile);
});

function buildUserProfile(user, data) {
    return {
        name: user.name,
        data: data
    };
}

なぜこれが重要なのか:リクエスト固有のデータはすべて、リクエストハンドラにスコープされた ローカル変数に格納される。各リクエストは、他の同時リクエストにリークすることのない、 孤立したステートを持ちます。関数はグローバルなステートにアクセスするのではなく、 パラメータを通してデータを受け取るので、競合状態が発生しません。

結論

リクエスト固有のデータはすべて、フレームワークが提供するローカル変数かリクエストオブジェクトに保持します。グローバル変数は、コンフィギュレーション、コネクションプール、読み取り専用キャッシュのような、本当に共有される状態のためだけに使用してください。グローバルな状態が必要な場合は、適切な同時実行制御を使用し、データがユーザー固有にならないようにします。

よくある質問

ご質問は?

Node.jsでグローバル変数を使うのはいつが安全ですか?

グローバル変数は、アプリケーション設定、データベース接続プール、 コンパイルされたテンプレート、共有ユーティリティなど、すべての リクエストに適用される読み取り専用データに対して安全です。リクエスト固有やユーザー固有のデータをグローバルに保存してはいけません。グローバルにデータをキャッシュする必要がある場合は、適切にキーが設定され、スレッドセーフにアクセスできることを確認するか、Redisのような適切なキャッシュソリューションを使用してください。

明示的にグローバルでないモジュールレベルの変数についてはどうだろうか?

モジュールレベルの変数(const、let、ファイルスコープのvar)は、Node.jsのグローバルのように動作します。これらはすべてのリクエストで持続し、すべての同時リクエストハンドラで共有されます。同じデータリークとレースコンディションのリスクが適用されます。モジュールレベル変数は、明示的グローバルと同じように注意して扱ってください。

ミドルウェアとルートハンドラ間でデータを共有するには?

フレームワークが提供するリクエストオブジェクトプロパティを使用する。Express は req.locals または req のカスタムプロパティを提供します。Fastify には request.decorateRequest() があります。これらのオブジェクトはリクエストにスコープされ、リクエスト完了後に自動的にクリーンアップされ、リクエスト間のリークを防ぎます。

シングルトン・パターンとクラス・インスタンスについてはどうだろう?

モジュールレベルのシングルトンインスタンスはグローバルステートである。もしリクエスト固有のデータを保持するのであれば、同じ問題が適用されます。シングルトンはステートレスにするか、コンフィギュレーションのみを保持するように設計しましょう。ステートフルな操作の場合は、リクエストごとに新しいインスタンスを生成するか、分離を保証するファクトリーパターンを使いましょう。

開発中にこのような問題を検出するには?

異なるユーザーコンテキストを使って、同時リクエストによる負荷テストを実行する。レースコンディションやデータリークは、逐次テストでは現れないことがよくあります。Apache Bench や autocannon のようなツールを使って並行負荷を生成してください。リクエスト ID を含むロギングを追加して、あるリクエストからのデータがいつ別のリクエストに現れたかを追跡する。

これはAWS Lambdaのようなサーバーレスファンクションにも適用されるのか?

部分的に。Lambdaを呼び出すたびに新しい実行環境が得られるが、コンテナは呼び出しをまたいで再利用できる。グローバル変数は、同じコンテナを再利用する呼び出し間で持続する。グローバル変数がリセットされることを当てにしてはいけない。リクエストデータはローカルスコープに保持する。

Python WSGI/ASGIアプリケーションについてはどうですか?

同じ原則が適用されます。Python Web サーバーはマルチスレッドまたは非同期で動作するので、モジュールレベルの変数はリクエスト間で共有されます。Flask の g オブジェクトと FastAPI の依存性注入は、リクエストスコープのストレージを提供します。Django にはリクエストオブジェクトがあります。リクエストデータにはモジュールグローバルの代わりにフレームワークが提供する メカニズムを使ってください。

まずは無料で体験

コード、クラウド、ランタイムを1つの中央システムでセキュアに。
脆弱性を迅速に発見し、自動的に修正。

クレジットカードは不要。