12k
All articles

Node.jsにおける安全なユーザー入力の取り扱い

Zodとパラメータクエリ、明示的な引数を用いた安全なNode.js入力処理により、SQLインジェクション、プロトタイプ汚染、マスアサインメント攻撃を防ぐ。

OpenReplay Team
OpenReplay Team
Node.jsにおける安全なユーザー入力の取り扱い

すべてのNode.js APIは、完全には信頼できないデータを受け取ります。JSONボディ、クエリ文字列、フォームフィールド、ファイルアップロードはすべて外部から到着し、そのいずれもが悪意のあるペイロードを含む可能性があります。SQLインジェクション、NoSQLインジェクション、コマンドインジェクション、プロトタイプ汚染 — これらの攻撃は共通のエントリーポイントを持っています:検証されていないユーザー入力がビジネスロジック、データベース、またはシェルコマンドに到達することです。この記事では、これらを防ぐ実践的なパターンを解説します。

重要なポイント

  • アプリケーションロジックに到達する前に、常に厳格なスキーマに対して受信データを検証する — 許可されるものを定義し、それ以外はすべて拒否します。
  • SQLにはパラメータ化されたクエリを使用し、NoSQLには厳格なデータ型を適用してインジェクション攻撃を防ぎます。
  • システムコマンドにはchild_process.exec()を避け、代わりに明示的な引数配列を持つexecFile()またはspawn()を使用します。
  • 生のリクエストボディをモデルに展開しない。期待するフィールドのみを選択して、マスアサインメントとプロトタイプ汚染を防ぎます。
  • ボディサイズの制限を設定し、パーサーレベルでファイルアップロードを検証して、サービス拒否攻撃から保護します。

最初に検証、次に処理

Node.js APIにおける安全なリクエスト処理で最も効果的なルールは:生のリクエストデータをアプリケーションロジックに触れさせないことです。使用する前に、すべての受信値の形状と型を検証してください。

スキーマベースの検証が最もクリーンなアプローチです。ZodJoiのようなライブラリを使用すると、リクエストがどのようなものであるべきかを正確に宣言し、一致しないものをすべて拒否できます。

import { z } from "zod"

const RegisterSchema = z.object({
  username: z.string().min(3).max(20).regex(/^[a-zA-Z0-9_]+$/),
  email: z.string().email(),
  password: z.string().min(10),
})

app.post("/register", (req, res) => {
  const result = RegisterSchema.safeParse(req.body)
  if (!result.success) {
    return res.status(400).json({ errors: result.error.flatten() })
  }
  // result.dataは安全に使用できます
})

これはホワイトリストアプローチです:許可されるものを定義し、それ以外はすべて拒否されます。これは危険な文字をブラックリスト化しようとするよりもはるかに信頼性が高い方法です。同じ原則はOWASP入力検証チートシートでも強調されています。


インジェクション攻撃を防ぐ

SQLおよびNoSQLインジェクション

ユーザー入力をクエリに連結しないでください。SQLにはパラメータ化されたステートメントを使用します:

// ❌ 脆弱
const query = `SELECT * FROM users WHERE email = '${req.body.email}'`

// ✅ 安全
const result = await db.query("SELECT * FROM users WHERE email = $1", [email])

NoSQLデータベースには独自のインジェクションリスクがあります。$gtのようなMongoDBオペレーターは、オブジェクトを直接渡すとJSONボディを通じてインジェクションされる可能性があります:

// ❌ 脆弱 — req.body.usernameが{ $gt: "" }である可能性がある
User.find({ username: req.body.username })

// ✅ 安全 — 最初に期待される型を強制する
User.find({ username: String(req.body.username) })

実際には、スキーマ検証により、クエリレイヤーに到達する前にフィールドが文字列であることを保証する必要があります(例えばz.string()を使用)。

コマンドインジェクション

APIがシステムコマンドを呼び出す場合、child_process.exec()は完全に避けてください — これはシェルを通じて引数を渡すため、簡単に悪用される可能性があります。代わりに明示的な引数配列を持つexecFile()またはspawn()を使用してください:

import { execFile } from "child_process"

// ✅ 引数は直接渡され、シェルによって解釈されません
execFile("convert", [userSuppliedFilename, outputPath], (err, stdout) => {
  // 結果を処理
})

それでも引数の値を検証しホワイトリスト化してください。操作がハイリスクの場合は、サンドボックス化されたコンテナ内で実行してください。


マスアサインメントとプロトタイプ汚染を避ける

マスアサインメントは、リクエストボディを直接モデルに展開したときに発生します:

// ❌ ユーザーが{ role: "admin" }を送信する可能性がある
const user = await User.create({ ...req.body })

// ✅ 期待するフィールドのみを選択
const user = await User.create({
  username: result.data.username,
  email: result.data.email,
})

プロトタイプ汚染はより微妙です。信頼できないオブジェクトをマージすると、攻撃者が__proto__constructor.prototypeのようなプロパティを注入し、プロセス内のすべてのオブジェクトに影響を与える可能性があります。生のリクエストデータに対してディープマージユーティリティを使用しないでください。マージが必要な場合は、まず入力を検証し、デフォルトのプロトタイプチェーンから継承しないようにObject.create(null)で中間オブジェクトを作成してください。OWASPプロトタイプ汚染防止チートシートがさらなるガイダンスを提供しています。


リクエストサイズと構造を制限する

大きなまたは深くネストされたペイロードは、サービス拒否の問題を引き起こす可能性があります。パーサーレベルでボディサイズの制限を設定してください:

app.use(express.json({ limit: "50kb" }))

ファイルアップロードには、明示的なfileSizefiles制限を持つmulterを使用し、MIMEタイプを検証してください — ファイル拡張子だけを信頼しないでください。


結論

Node.jsにおける安全なユーザー入力の取り扱いは、1つの原則に集約されます:有効な入力がどのようなものかを定義し、境界でそれを強制し、それ以外を通さないことです。スキーマ検証、パラメータ化されたクエリ、明示的な引数の渡し方、厳格なフィールド選択は、実世界の攻撃面の大部分をカバーします。これらのパターンをリクエストレイヤーで一貫して適用し、データがダウンストリームロジックに到達する前に実行すれば、ほとんどのインジェクションクラスの脆弱性は単に着地する場所がなくなります。

よくある質問

スキーマ検証だけですべてのインジェクション攻撃を防ぐことができますか?

スキーマ検証は、不正な形式の入力がロジックに到達する前に拒否するため、強力な第一線の防御です。ただし、パラメータ化されたクエリと安全なコマンド実行と組み合わせる必要があります。検証はデータが正しい形状と型を持つことを保証しますが、ダウンストリームのコードもそのデータを安全に処理する必要があり、検証だけでは見逃す可能性のあるエッジケースをカバーします。

入力検証にZodとJoiのどちらを使用すべきですか?

両方のライブラリは成熟しており、広く使用されています。Zodは強力な型推論を持つファーストクラスのTypeScriptサポートを提供し、TypeScriptプロジェクトに自然にフィットします。JoiはExpressエコシステムでより長い実績があり、豊富でチェーン可能なAPIを提供します。プロジェクトの言語の好みと既存の依存関係に基づいて選択してください。

オブジェクトをディープマージする必要がある場合、プロトタイプ汚染から保護するにはどうすればよいですか?

生のリクエストデータに対してディープマージ関数を実行しないでください。まず厳格なスキーマで入力を検証し、既知のフィールドのみが通過するようにします。マージが必要な場合は、Object.create(null)で中間オブジェクトを作成し、プロトタイプチェーンのないオブジェクトを生成します。lodash.mergeのようなライブラリは過去にプロトタイプ汚染の脆弱性があったため、常に依存関係を最新の状態に保ってください。

一般的なJSON APIにはどのようなボディサイズ制限を設定すべきですか?

普遍的な答えはありませんが、100kbから1MBがほとんどのJSON APIのユースケースをカバーします。100kbのような控えめな制限から始め、正当なリクエストがより多くを必要とする場合にのみ増やしてください。ファイルアップロードの場合は、マルチパートパーサーを通じて別の制限を設定してください。目標は、メモリや処理時間を消費する前に、異常に大きなペイロードを早期に拒否することです。

DevTools for the frontend

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.