入力検証とは、アプリケーションが処理する前に、受け取ったデータが期待される形式、型、値の範囲に適合しているかをチェックするプロセスです。OWASP Input Validation Cheat Sheet (2025)によると、検証の欠如はほとんどの重大な脆弱性の根本原因です。受け取ったデータのチェックは防御の第一線であり、不正または悪意のあるデータがシステムに侵入するのを防ぎます。
重要なポイント
入力検証とは、ユーザー、外部サービス、または別のコンポーネントからアプリケーションに入るデータが期待される基準に適合しているかをチェックすることです。これらの基準には、データ型(文字列、数値、日付)、形式(メール、URL、電話)、値の範囲(年齢18〜120)、長さ(パスワード8〜128文字)、許可される文字(ラテン文字のみ、数字、ハイフン)が含まれます。検証がないと、アプリケーションは実行時エラー、データ破損、またはセキュリティ脆弱性を引き起こすデータを処理する可能性があります。
入力検証の欠如は、SQL Injection、XSS、Command Injection、Path Traversal、Buffer Overflowなどの脆弱性の根本原因です。MITRE CWE (2025)によると、CWE-20(Improper Input Validation)は最も危険なソフトウェアエラーのランキングで2位です。検証はDefense in Depthセキュリティモデルにおける防御の第一線です。不正なデータがシステムの他のコンポーネントに到達する前に遮断します。
検証は基準を満たさないデータを拒否します。サニタイゼーション(クレンジング)は危険な部分を削除またはエスケープしてデータを修正します。たとえば、HTMLコンテンツを入力する場合、検証はテキストの長さをチェックし、サニタイゼーションはHTML PurifierやDOMPurifyライブラリを使用してscriptタグを削除します。サニタイゼーションは検証の代わりにはなりません。両者は連携して機能します。検証は「許可/禁止」ポリシーであり、サニタイゼーションは「使用前にクレンジング済み」です。
検証はチェックの深さによって分類されます。フォーマット検証は最も簡単で高速であり、ビジネス検証は最も複雑で文脈依存です。3つのレベルすべてを順番に適用する必要があります。まずフォーマット、次に意味、次にビジネスロジックです。いずれかのレベルをスキップすると、システムの誤動作や脆弱性につながる可能性があります。
| レベル | チェック内容 | 例 |
|---|---|---|
| フォーマット | データ型、長さ、正規表現 | メールに@が含まれる、長さ5-100 |
| 意味 | 値の論理的正当性 | 生年月日が未来ではない |
| ビジネス検証 | ビジネスルールへの適合 | 送金額が残高を超えない |
データ型、サイズ、形式、許可される文字のチェック。正規表現、言語の組み込み型、検証ライブラリを使用して実装されます。例: UUIDのチェック(8-4-4-4-12の16進数字の形式)、電話番号のチェック(数字のみ、先頭に+、7〜15文字)、整数のチェック(値がInteger.MIN_VALUE — Integer.MAX_VALUEの範囲内)。フォーマット検証は、すべての入力フィールドの最小限必要なレベルです。
ドメインの文脈におけるデータの論理的正当性のチェック。たとえば、開始日が終了日より後でないこと、年齢がシステムにとって妥当な範囲内であること、座標がサービスエリア内であること。意味検証にはビジネスコンテキストの理解が必要であり、形式だけでは実行できません。たとえば、「チケット数」フィールドはフォーマット検証(整数、> 0)を通過しても、意味的には利用可能な座席数を超えることはできません。
最も複雑なレベル — アプリケーションのビジネスルールへのデータ適合をチェックします。例: ユーザーは唯一の管理者を削除できない、注文金額がクレジット限度額を超えない、商品は在庫がある場合のみ注文できる。ビジネス検証には多くの場合データベースクエリや外部サービスが必要であり、フォーマット検証と意味検証の後に実行されます。ビジネス検証のエラーは、ユーザー不満の最も一般的な原因です。
クライアント側の検証(ブラウザやモバイルアプリ内)はユーザーの利便性のために必要です。データをサーバーに送信せずに即座にフィードバックが得られます。ただし、サーバー側の検証のみが唯一信頼できるものです。クライアントコードは常に回避できるからです。開発者ツール、Postman、プロキシ(Burp Suite)を介してリクエストを送信すれば、クライアント側の検証は存在しなくなります。PortSwigger Research (2025)によると、テストされたWebアプリケーションの90%以上が、少なくとも1つのフィールドでクライアント側の検証のみに依存しています。
クライアント側の検証では、送信ボタンを無効化したり、エラーを強調表示したり、ヒントを表示したりできます。サーバー側の検証は、クライアントがすでにチェックした場合でも、すべてのパラメータの必須チェックです。両方のレベルで検証を重複させるのは標準的なプラクティスです。サーバーはクライアントが存在しないかのようにデータをチェックする必要があります。これにより、改変されたリクエスト、自動化された攻撃、悪意のあるクライアントからの保護が保証されます。
Webでは — HTML5属性(required、pattern、min/max、type="email")とJavaScript。モバイルアプリでは — ネイティブのテキストフィールドバリデータ(AndroidのInputFilter、iOSのtextField(:shouldChangeCharactersIn:))。React Hook FormとFormik(React用)、Vuelidate(Vue用)、Angular Reactive Formsは、クライアント側検証の一般的なライブラリです。これらはすべてカスタムルールと非同期検証(サーバーでのログインの一意性チェック)をサポートしています。
// ExpressとJoiを使ったサーバー側検証の例
const Joi = require('joi');
const userSchema = Joi.object({
email: Joi.string()
.email()
.required()
.max(255),
age: Joi.number()
.integer()
.min(18)
.max(120)
.required(),
password: Joi.string()
.pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,128}$/)
.required()
});
app.post('/api/users', async (req, res) => {
const { error, value } = userSchema.validate(req.body);
if (error) {
return res.status(400).json({
error: error.details[0].message
});
}
// value — すでに検証済みで安全なデータ
const user = await User.create(value);
res.status(201).json(user);
});
モバイルアプリはデータ検証に特別な要件を課します。画面が小さいため、エラーは簡潔に、キーボードは文脈に応じたもの(数字入力には数字キーボード)にし、UIをブロックしないようにチェックを非同期にする必要があります。ネイティブプラットフォームはデフォルトで使用すべき組み込みの検証メカニズムを提供しています。AndroidのMaterial Design GuidelinesとiOSのHuman Interface Guidelinesには、検証エラーの表示に関する詳細な推奨事項が含まれています。
Jetpack Composeは状態管理を通じて宣言的な検証アプローチを提供します。各入力フィールドは状態(MutableState)にバインドされ、エラーは現在の値に基づいて計算されます。Compose Validatorライブラリはルールの作成を簡素化します: required、email、min/max length、pattern。検証はテキストの変更時(onValueChange)またはフォーム送信の試行時にトリガーされます。最初の送信後、またはユーザーが入力し終えた後にのみエラーを表示することをお勧めします(デバウンス300-500ms)。
SwiftUIには組み込みのフォーム検証メカニズムはありませんが、Combineとプロパティラッパーを使用して簡単に実装できます。フィールドの値には@State、エラーには計算プロパティを使用します。ValidatedPropertyKitフレームワークは、@Validated().email()、@Validated().range(18...120)などの既製のデコレータを提供します。iOSの推奨事項 — キーボードタイプ(UIKeyboardType.emailAddress、.numberPad)と自動大文字化を使用して、入力レベルでのエラー数を減らすことです。
Flutterは、バリデータコールバックによる組み込み検証を備えたFormクラスとTextFormFieldクラスを提供します。各フィールドは、データが正しい場合はエラーを文字列として、正しくない場合はnullを返します。FormState.validate()はフォームのすべてのフィールドの検証を実行します。複雑なケース用のreactive_formsパッケージ: カスタムバリデータ、非同期検証、動的ルール。Flutter Webとモバイル版は同じAPIを使用するため、メンテナンスが簡単です。
// Flutterでのフォーム検証の例
Form(
key: _formKey,
child: Column(
children: [
TextFormField(
decoration: InputDecoration(labelText: 'Email'),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Email is required';
}
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$')
.hasMatch(value)) {
return 'Enter a valid email';
}
return null;
},
),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
// 有効なデータを処理する
}
},
child: Text('送信'),
),
],
),
)
最新のフレームワークは、ニーズの80%をカバーする組み込みバリデータを提供します。残りの20%にはカスタムルール、正規表現、または既存ルールの組み合わせが必要です。重要な原則は、検証を宣言的にして、簡単に読み、テストし、保守できるようにすることです。コントローラや画面に分散した検証ロジックを避け、個別のクラスやスキーマに移してください。
| ツール | プラットフォーム | 特徴 |
|---|---|---|
| Joi | Node.js | 宣言的スキーマ、カスタムメッセージ |
| Pydantic | Python | 型ヒント、モデルの自動検証 |
| Zod | TypeScript | 型推論、厳密な型付け |
| javax.validation | Java | Bean Validation、@NotNull、@Size、@Pattern |
| FluentValidation | .NET | Fluent API、rulesets、条件付きルール |
ホワイトリスト — 許可するデータを定義し、それ以外はすべて拒否します。ブラックリスト — 禁止するデータを定義し、それ以外はすべて許可します。ホワイトリストは常に信頼性が高い: どのデータが通過するかを正確に把握できます。ブラックリストでは考えられるすべての攻撃を予測する必要があり、それは不可能です。例: 年齢をチェックする場合は、ブラックリスト("0"、"-1"、"999999"を禁止)ではなくホワイトリスト(18〜120の数字のみ)を使用します。
正規表現はフォーマット検証に効果的なツールですが、ReDoS攻撃(Regular Expression Denial of Service)の原因になる可能性があります。一部のパターン(たとえば、(a+)+b)は長い文字列で壊滅的なバックトラッキングを引き起こし、サーバーのCPUを完全に占有します。実績のあるregexライブラリを使用し、正規表現を適用する前に文字列の長さを制限してください。複雑なケース(メール、URL)では、自作の正規表現ではなく、言語の組み込みパーサーを使用してください。
経験豊富な開発者でさえ、検証の実装時に間違いを犯します。最も一般的なもの: クライアントのみの検証、過度に厳格なルール(パスワード「Must contain uppercase, lowercase, digit, special char, >= 12 chars, must not repeat characters」)、情報のないエラーメッセージ("Error: invalid input")、エッジケースの無視(先頭/末尾のスペース、Unicode文字、空文字列)。これらの間違いはそれぞれUXを悪化させ、フォームのコンバージョンを低下させる可能性があります。
if (value)は空文字列とゼロ、false、「0」を区別しないベストプラクティスは、単体テストでカバーされた集中型の検証システムです。各ルールは個別にテストする必要があります: 境界値、正しいデータ、典型的な攻撃(SQLi試行、XSSペイロード、非常に長い文字列)。検証の回帰テストは、リファクタリング中のルールの偶発的な弱体化を防ぎます。プロパティベーステスト(QuickCheck、fast-check)を使用してランダムデータを生成し、検証が例外で失敗しないことを確認してください。
よくある質問
検証は不正なデータを拒否し、サニタイゼーションはそれをクレンジングします。たとえば、HTMLテキストを入力する場合、検証は最大長をチェックし、サニタイゼーションはDOMPurifyでscriptタグを削除します。両方のプロセスが必須です: 検証はフォーマット制御用、サニタイゼーションは出力セキュリティ用です。
いいえ、決して十分ではありません。クライアント側の検証は、リクエストの傍受と改変によって簡単に回避されます。Burp Suiteなどのツールや単純なcurlを使用してください。サーバー側の検証だけがシステムを保護する唯一の信頼できる方法です。クライアント側の検証はユーザー体験の向上のみを目的としています。
MIMEタイプ(拡張子だけでなく)、ファイルサイズ、シグネチャ(ファイル先頭のマジックバイト)をファイルシグネチャ検証でチェックします。拡張子を決して信頼しないでください — 保存時にファイル名を変更してください。画像の場合は、サーバーライブラリ(ImageMagick、Sharp)で再エンコードし、EXIFデータから埋め込まれたコードを削除してください。
ReDoS(Regular Expression Denial of Service)は、攻撃者が特別に細工した文字列を送信し、正規表現で壊滅的なバックトラッキングを引き起こす攻撃です。その結果、サーバーのCPUが100%に達し、応答が生成されません。対策: 文字列の長さの制限、regexのタイムアウト、実績のあるパターンの使用。
はい、データがWebViewで表示されたり、HTMLコンテキストで使用される場合は必要です。バックエンドが侵害されると、データに悪意のあるコードが含まれる可能性があります。ソースに関係なく、ユーザーに表示されるすべてのデータを検証し、サニタイズしてください。モバイルアプリでは、これはハイブリッドコンポーネントにとって特に重要です。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。