# PHP/Laravel セキュリティリンター 検出ルール一覧 このドキュメントでは、セキュリティリンターが検出できる脆弱性パターンを詳細に説明します。 --- ## 目次 1. [XSS (クロスサイトスクリプティング)](#1-xss-クロスサイトスクリプティング) 2. [SQLインジェクション](#2-sqlインジェクション) 3. [コマンドインジェクション](#3-コマンドインジェクション) 4. [パストラバーサル](#4-パストラバーサル) 5. [認証セキュリティ](#5-認証セキュリティ) 6. [CSRF/セッションセキュリティ](#6-csrfセッションセキュリティ) 7. [設定セキュリティ](#7-設定セキュリティ) --- ## 1. XSS (クロスサイトスクリプティング) ### 1.1 Blade テンプレートの生出力 | パターン | 重大度 | 説明 | |---------|--------|------| | `{!! $var !!}` | HIGH | エスケープされていない変数の出力 | | `{!! $array['key'] !!}` | HIGH | 配列アクセスの生出力 | | `{!! $obj->prop !!}` | HIGH | プロパティアクセスの生出力 | **安全と判定されるパターン:** ```blade {{-- エスケープ関数でラップされている場合 --}} {!! htmlspecialchars($var) !!} {!! e($var) !!} {!! htmlentities($var) !!} {!! strip_tags($html) !!} {{-- 文字列リテラルとエスケープ値の連結 --}} {!! '
' . htmlspecialchars($name) . '
' !!} {{-- Laravel 組み込みの安全な出力 --}} {!! csrf_field() !!} {!! method_field('PUT') !!} {!! $errors !!} {!! $slot !!} {{-- Markdown プロセッサ --}} {!! Markdown::parse($content) !!} {!! Parsedown::instance()->text($content) !!} {{-- サニタイズ関数 --}} {!! clean($html) !!} {!! sanitize($html) !!} {!! purify($html) !!} ``` ### 1.2 エスケープ破壊関数の検出 エスケープ処理を無効化する関数呼び出しを検出します。 | 関数 | 説明 | |------|------| | `html_entity_decode()` | HTMLエンティティをデコード | | `htmlspecialchars_decode()` | htmlspecialchars の逆変換 | | `urldecode()` | URLエンコードをデコード | | `rawurldecode()` | rawurlencode の逆変換 | | `base64_decode()` | Base64 デコード | | `json_decode()` | JSON デコード | | `stripslashes()` | スラッシュを除去 | | `stripcslashes()` | C スタイルのスラッシュを除去 | **検出例:** ```blade {{-- 危険: エスケープ後にデコードしている --}} {!! html_entity_decode(htmlspecialchars($data)) !!} {!! urldecode(htmlspecialchars($url)) !!} ``` ### 1.3 危険なハードコード HTML の検出 文字列リテラル内の危険な HTML を検出します。 | 検出パターン | 説明 | |-------------|------| | `' . htmlspecialchars($input); } // 安全: 危険なタグを含まない function safeFormatter($input) { return '
' . htmlspecialchars($input) . '
'; } ``` ### 1.4 JavaScript コンテキスト | パターン | 重大度 | 説明 | |---------|--------|------| | `` | MEDIUM | script 内の Blade 出力 | | `` | CRITICAL | script 内の生出力 | | `@json($var)` in ` ``` ### 1.5 URL コンテキスト `javascript:` URL によるXSSを検出します。 | パターン | 重大度 | |---------|--------| | `href="{{ $url }}"` | MEDIUM | | `src="{{ $url }}"` | MEDIUM | | `action="{{ $url }}"` | MEDIUM | | `formaction="{{ $url }}"` | MEDIUM | **安全と判定されるパターン:** ```blade {{-- route() や url() ヘルパーは安全 --}} Home About
``` ### 1.6 イベントハンドラ | パターン | 重大度 | |---------|--------| | `onclick="{{ $var }}"` | HIGH | | `onerror="{{ $var }}"` | HIGH | | `onload="{{ $var }}"` | HIGH | | `onclick="{!! $var !!}"` | CRITICAL | **推奨される対策:** ```blade {{-- 危険 --}} {{-- 推奨: data 属性を使用 --}} ``` ### 1.7 Style インジェクション | パターン | 重大度 | |---------|--------| | `style="{{ $css }}"` | MEDIUM | | `style="color: {{ $color }}"` | MEDIUM | ### 1.8 引用符なし属性 | パターン | 重大度 | |---------|--------| | `data-value={{ $var }}` | LOW | ### 1.9 テンプレートインジェクション | パターン | 重大度 | |---------|--------| | `@include($var)` | HIGH | | `@extends($var)` | HIGH | | `@component($var)` | HIGH | | `@each($var, ...)` | HIGH | ### 1.10 SVG コンテキスト | パターン | 重大度 | |---------|--------| | `{{ $var }}` | MEDIUM | ### 1.11 @php ブロック | パターン | 重大度 | |---------|--------| | `@php echo $var; @endphp` | MEDIUM | ### 1.12 PHP の echo/print | パターン | 重大度 | |---------|--------| | `echo $taintedVar;` | HIGH | | `print $taintedVar;` | HIGH | | `printf("%s", $taintedVar)` | HIGH | ### 1.13 ユーザー定義関数の再帰解析 リンターは、ユーザー定義関数を再帰的に解析して、戻り値がエスケープされているかを判定します。 ```php // 安全と判定: 戻り値が常にエスケープされている function safeFormatter($input) { return htmlspecialchars(trim($input)); } // 危険と判定: エスケープされていない戻り値がある function unsafeFormatter($input) { return trim($input); // エスケープなし } // 危険と判定: エスケープを壊している function breakEscaping($escaped) { return html_entity_decode($escaped); } ``` --- ## 2. SQLインジェクション ### 2.1 Laravel クエリビルダー | パターン | 重大度 | |---------|--------| | `DB::raw($userInput)` | HIGH | | `DB::select($query . $userInput)` | HIGH | | `->whereRaw($userInput)` | HIGH | | `->selectRaw($userInput)` | HIGH | | `->orderByRaw($userInput)` | HIGH | | `->havingRaw($userInput)` | HIGH | | `->groupByRaw($userInput)` | HIGH | **安全なパターン:** ```php // パラメータバインディングを使用 DB::select('SELECT * FROM users WHERE id = ?', [$id]); $query->whereRaw('column = ?', [$value]); ``` ### 2.2 PDO | パターン | 重大度 | |---------|--------| | `$pdo->query($sql . $input)` | HIGH | | `$pdo->exec($sql . $input)` | HIGH | | `$pdo->prepare($sql . $input)` | HIGH | ### 2.3 MySQLi | パターン | 重大度 | |---------|--------| | `$mysqli->query($sql . $input)` | HIGH | | `mysqli_query($conn, $sql . $input)` | HIGH | ### 2.4 文字列連結によるクエリ構築 | パターン | 重大度 | |---------|--------| | `"SELECT * FROM users WHERE id = " . $id` | HIGH | | `"SELECT * FROM users WHERE id = {$id}"` | HIGH | ### 2.5 SQLサニタイザー関数の検出 リンターは以下のサニタイザー関数を認識し、適切に使用されている場合は安全と判定します。 | 関数/メソッド | 説明 | |--------------|------| | `intval()`, `floatval()` | 型キャスト(最も安全) | | `(int)`, `(float)` | 型キャスト演算子 | | `mysqli_real_escape_string()` | MySQLi エスケープ | | `PDO::quote()` | PDO エスケープ | | `pg_escape_string()`, `pg_escape_literal()` | PostgreSQL エスケープ | | `addslashes()` | 基本的なエスケープ(非推奨) | | `filter_var()` + FILTER_VALIDATE_INT | 検証フィルター | **安全と判定されるパターン:** ```php // 型キャストによるサニタイズ $query = "SELECT * FROM users WHERE id = " . intval($id); $query = "SELECT * FROM users WHERE id = " . (int)$id; // エスケープ関数 $query = "SELECT * FROM users WHERE name = '" . mysqli_real_escape_string($conn, $name) . "'"; $query = "SELECT * FROM users WHERE name = " . $pdo->quote($name); // ユーザー定義サニタイザー関数 function sanitizeId($input) { return intval($input); } $query = "SELECT * FROM users WHERE id = " . sanitizeId($userId); ``` ### 2.6 SQLサニタイザー破壊パターン サニタイズ後に以下の関数を使用すると、サニタイズが無効化されます。 | 関数 | 説明 | |------|------| | `stripslashes()` | エスケープを解除 | | `urldecode()` | URLデコードで特殊文字を復活 | | `html_entity_decode()` | HTMLエンティティをデコード | | `base64_decode()` | Base64 デコード | | `sprintf()` | フォーマット文字列で迂回可能 | **危険なパターン:** ```php // 危険: サニタイズ後にデコード $safe = mysqli_real_escape_string($conn, $input); $unsafe = stripslashes($safe); // エスケープを解除 // 危険: urldecode でエスケープを迂回 $safe = addslashes($input); $unsafe = urldecode($safe); // %27 -> ' に変換 ``` ### 2.7 ユーザー定義関数の再帰解析 リンターはユーザー定義関数を再帰的に解析し、安全かどうかを判定します。 ```php // 安全と判定: 戻り値が常にサニタイズされている function getIntParam($key) { return intval($_GET[$key] ?? 0); } // 危険と判定: サニタイズを破壊している function processInput($input) { $escaped = mysqli_real_escape_string($this->conn, $input); return stripslashes($escaped); // エスケープを解除! } ``` --- ## 3. コマンドインジェクション ### 3.1 シェル実行関数 | 関数 | 重大度 | |------|--------| | `exec($cmd . $input)` | CRITICAL | | `shell_exec($cmd . $input)` | CRITICAL | | `system($cmd . $input)` | CRITICAL | | `passthru($cmd . $input)` | CRITICAL | | `proc_open($cmd . $input, ...)` | CRITICAL | | `popen($cmd . $input, ...)` | CRITICAL | | `` `$cmd $input` `` (バッククォート) | CRITICAL | ### 3.2 コード実行関数 | 関数 | 重大度 | |------|--------| | `eval($code)` | CRITICAL | | `create_function($args, $code)` | CRITICAL | | `assert($expr)` (文字列引数) | CRITICAL | | `preg_replace('/.../e', ...)` | CRITICAL | ### 3.3 コールバック関数 | 関数 | 重大度 | |------|--------| | `call_user_func($userCallback)` | HIGH | | `call_user_func_array($userCallback, ...)` | HIGH | | `array_map($userCallback, ...)` | HIGH | | `array_filter($arr, $userCallback)` | HIGH | ### 3.4 ファイルインクルード | 関数 | 重大度 | |------|--------| | `include($userPath)` | CRITICAL | | `include_once($userPath)` | CRITICAL | | `require($userPath)` | CRITICAL | | `require_once($userPath)` | CRITICAL | ### 3.5 Symfony Process | パターン | 重大度 | |---------|--------| | `Process::fromShellCommandline($cmd . $input)` | HIGH | | `new Process($stringCmd . $input)` | HIGH | **安全なパターン:** ```php // 配列引数を使用 $process = new Process(['command', $arg1, $arg2]); ``` ### 3.6 Laravel Artisan | パターン | 重大度 | |---------|--------| | `Artisan::call($userCommand)` | HIGH | ### 3.7 コマンドサニタイザー関数の検出 リンターは以下のサニタイザー関数を認識し、適切に使用されている場合は安全と判定します。 | 関数 | 説明 | |------|------| | `escapeshellarg()` | 単一の引数をエスケープ | | `escapeshellcmd()` | コマンド全体をエスケープ | | `basename()` | ファイル名部分のみ抽出 | | `intval()`, `floatval()` | 型キャスト | **安全と判定されるパターン:** ```php // escapeshellarg で引数をエスケープ exec('ls -la ' . escapeshellarg($path)); // escapeshellcmd でコマンド全体をエスケープ exec(escapeshellcmd($command) . ' ' . escapeshellarg($arg)); // 型キャストで数値に限定 exec('kill ' . intval($pid)); // 配列引数による安全な Process 使用 $process = new Process(['convert', $inputFile, $outputFile]); ``` ### 3.8 コマンドサニタイザー破壊パターン サニタイズ後に以下の関数を使用すると、サニタイズが無効化されます。 | 関数 | 説明 | |------|------| | `stripslashes()` | エスケープを解除 | | `urldecode()` | URLデコードで特殊文字を復活 | | `str_replace()` | エスケープ文字を削除可能 | | `preg_replace()` | エスケープ文字を削除可能 | | `sprintf()` | フォーマット文字列で迂回可能 | **危険なパターン:** ```php // 危険: サニタイズ後にデコード $safe = escapeshellarg($input); $unsafe = urldecode($safe); // %20 -> スペースに変換 // 危険: サニタイズを壊す置換 $safe = escapeshellarg($input); $unsafe = str_replace("'", "", $safe); // エスケープを除去 // 危険: sprintf でフォーマット文字列攻撃 $cmd = sprintf($userFormat, escapeshellarg($arg)); // $userFormat が制御可能 ``` ### 3.9 ユーザー定義関数の再帰解析 リンターはユーザー定義関数を再帰的に解析し、安全かどうかを判定します。 ```php // 安全と判定: 戻り値が常にサニタイズされている function safeCommand($input) { return escapeshellarg(trim($input)); } // 危険と判定: サニタイズを破壊している function processCommand($input) { $escaped = escapeshellarg($input); return urldecode($escaped); // エスケープを解除! } ``` --- ## 4. パストラバーサル ### 4.1 ファイル操作関数 | 関数 | 重大度 | |------|--------| | `file_get_contents($userPath)` | HIGH | | `file_put_contents($userPath, ...)` | HIGH | | `fopen($userPath, ...)` | HIGH | | `readfile($userPath)` | HIGH | | `unlink($userPath)` | HIGH | | `copy($userPath, ...)` | HIGH | | `rename($userPath, ...)` | HIGH | ### 4.2 ファイルアップロード | パターン | 重大度 | |---------|--------| | `move_uploaded_file($tmp, $userDest)` | HIGH | ### 4.3 Laravel Storage | パターン | 重大度 | |---------|--------| | `Storage::get($userPath)` | MEDIUM | | `Storage::put($userPath, ...)` | MEDIUM | | `Storage::delete($userPath)` | MEDIUM | | `Storage::download($userPath)` | MEDIUM | ### 4.4 レスポンスダウンロード | パターン | 重大度 | |---------|--------| | `response()->download($userPath)` | HIGH | | `response()->file($userPath)` | HIGH | ### 4.5 パスサニタイザー関数の検出 リンターは以下のサニタイザー関数を認識し、適切に使用されている場合は安全と判定します。 | 関数 | 説明 | |------|------| | `basename()` | ディレクトリ成分を除去(最も効果的) | | `realpath()` | パスを正規化し、存在を確認 | | `pathinfo(..., PATHINFO_BASENAME)` | ファイル名部分のみ抽出 | | `intval()` | 数値IDに限定 | | `Str::random()`, `Str::uuid()` | 安全なファイル名生成 | | `$file->hashName()` | ハッシュベースの安全なファイル名 | **安全と判定されるパターン:** ```php // basename でディレクトリを除去 $filename = basename($userInput); file_get_contents($basePath . '/' . $filename); // realpath で検証 $path = realpath($basePath . '/' . $userInput); if ($path && str_starts_with($path, $basePath)) { file_get_contents($path); } // 数値IDに限定 $content = file_get_contents('/docs/' . intval($id) . '.txt'); // 安全なファイル名生成 $filename = Str::random(40) . '.pdf'; Storage::put($filename, $content); // ハッシュベースのファイル名 $path = $request->file('upload')->hashName(); ``` ### 4.6 危険なパストラバーサルパターン 以下のパターンが文字列リテラルに含まれている場合、警告します。 | パターン | 説明 | |---------|------| | `..` | ディレクトリトラバーサル | | `../`, `..\\` | Unix/Windows トラバーサル | | `%2e%2e` | URLエンコードされた `..` | | `%252e%252e` | ダブルURLエンコード | | `..%c0%af`, `..%c1%9c` | オーバーロング UTF-8 エンコード | ### 4.7 パスサニタイザー破壊パターン サニタイズ後に以下の関数を使用すると、サニタイズが無効化されます。 | 関数 | 説明 | |------|------| | `urldecode()` | `%2e%2e` → `..` | | `rawurldecode()` | URLデコード | | `base64_decode()` | Base64 でトラバーサルを隠蔽可能 | | `hex2bin()` | 16進数でトラバーサルを隠蔽可能 | | `html_entity_decode()` | HTMLエンティティをデコード | | `chr()` | 文字を構築可能 | **危険なパターン:** ```php // 危険: サニタイズ後にデコード $safe = basename($userInput); $unsafe = urldecode($safe); // %2e%2e -> .. に変換 // 危険: base64 でトラバーサルを隠蔽 $path = base64_decode($userInput); // Li4vLi4vZXRjL3Bhc3N3ZA== -> ../../etc/passwd // 危険: サニタイズ前に getClientOriginalName を使用 $filename = $file->getClientOriginalName(); // ユーザー制御! Storage::put($filename, $content); // トラバーサル可能 ``` ### 4.8 ユーザー定義関数の再帰解析 リンターはユーザー定義関数を再帰的に解析し、安全かどうかを判定します。 ```php // 安全と判定: 戻り値が常にサニタイズされている function safePath($input) { return basename(trim($input)); } // 危険と判定: サニタイズを破壊している function processPath($input) { $safe = basename($input); return urldecode($safe); // サニタイズを解除! } // 安全と判定: 検証ロジックを含む function validatePath($input, $baseDir) { $path = realpath($baseDir . '/' . $input); if ($path && str_starts_with($path, $baseDir)) { return $path; } return null; } ``` --- ## 5. 認証セキュリティ ### 5.1 弱いハッシュアルゴリズム | 関数 | 重大度 | |------|--------| | `md5($password)` | HIGH | | `sha1($password)` | HIGH | | `sha256($password)` | MEDIUM | | `hash('md5', $password)` | HIGH | ### 5.2 不適切なパスワードハッシュ | パターン | 重大度 | |---------|--------| | `password_hash($pw, PASSWORD_MD5)` | HIGH | | `password_hash($pw, ..., ['cost' => 4])` | MEDIUM | ### 5.3 ハードコードされた認証情報 | パターン | 重大度 | |---------|--------| | `$password = 'secret123'` | HIGH | | `$apiKey = 'sk-xxx...'` | HIGH | | `['password' => 'hardcoded']` | HIGH | ### 5.4 タイミング攻撃 | パターン | 重大度 | |---------|--------| | `$token == $userToken` | MEDIUM | | `strcmp($secret, $input)` | MEDIUM | **推奨:** ```php // 定時間比較を使用 hash_equals($expected, $actual); password_verify($password, $hash); ``` ### 5.5 Base64 エンコード | パターン | 重大度 | |---------|--------| | `base64_encode($password)` | HIGH | --- ## 6. CSRF/セッションセキュリティ ### 6.1 CSRF トークン | パターン | 重大度 | |---------|--------| | `` without `@csrf` | HIGH | | Form missing `csrf_field()` | HIGH | ### 6.2 メソッドスプーフィング | パターン | 重大度 | |---------|--------| | PUT/PATCH/DELETE form without `@method` | LOW | ### 6.3 セッション設定 | パターン | 重大度 | |---------|--------| | `session_start()` without options | MEDIUM | | Missing `cookie_httponly` | MEDIUM | | Missing `cookie_secure` | MEDIUM | | Missing `cookie_samesite` | MEDIUM | ### 6.4 セッション固定化 | パターン | 重大度 | |---------|--------| | `session_regenerate_id()` without `true` | MEDIUM | | `session_regenerate_id(false)` | MEDIUM | ### 6.5 Cookie セキュリティ | パターン | 重大度 | |---------|--------| | Cookie without `httponly` | MEDIUM | | Cookie without `secure` | MEDIUM | | Cookie without `samesite` | MEDIUM | --- ## 7. 設定セキュリティ ### 7.1 デバッグ出力 | 関数 | 重大度 | |------|--------| | `phpinfo()` | HIGH | | `var_dump($var)` | MEDIUM | | `print_r($var)` | MEDIUM | | `dd($var)` | MEDIUM | | `dump($var)` | MEDIUM | ### 7.2 エラー表示 | パターン | 重大度 | |---------|--------| | `error_reporting(-1)` | MEDIUM | | `ini_set('display_errors', '1')` | MEDIUM | | `ini_set('display_startup_errors', '1')` | MEDIUM | ### 7.3 安全でないデシリアライズ | パターン | 重大度 | |---------|--------| | `unserialize($data)` | HIGH | | `unserialize($data, ['allowed_classes' => true])` | HIGH | **安全なパターン:** ```php unserialize($data, ['allowed_classes' => false]); unserialize($data, ['allowed_classes' => [AllowedClass::class]]); ``` ### 7.4 機密情報のログ出力 | パターン | 重大度 | |---------|--------| | `Log::info($password)` | MEDIUM | | `logger($apiKey)` | MEDIUM | ### 7.5 ハードコードされた設定 | パターン | 重大度 | |---------|--------| | `'debug' => true` | MEDIUM | | `'key' => 'hardcoded-secret'` | HIGH | --- ## 再帰的なテイント解析 リンターは、関数呼び出しを通じてユーザー入力(テイントデータ)がどのように伝播するかを追跡します。 ### テイントソース(汚染源) ```php $_GET, $_POST, $_REQUEST, $_COOKIE, $_FILES, $_SERVER $request->input() $request->get() $request->post() $request->query() $request->all() $request->only() $request->except() $request->file() $request->cookie() request()->input() Request::input() file_get_contents('php://input') ``` ### サニタイザー(浄化関数) 以下の関数を通過したデータは、対応する脆弱性に対して安全と判定されます: ```php // XSS htmlspecialchars(), htmlentities(), strip_tags(), e() // SQL addslashes(), mysqli_real_escape_string(), pg_escape_string() // パス basename(), realpath() // 型キャスト (int), (float), (bool), intval(), floatval(), boolval() // バリデーション validate(), validated() ``` ### 解析の深度 デフォルトで最大10レベルの関数呼び出しを追跡します。 ```php // 例: 3レベルの追跡 function level1($input) { return level2($input); } function level2($input) { return level3($input); } function level3($input) { return $input; } // テイント echo level1($_GET['input']); // 検出される ``` --- ## 使用例 ```bash # 単一ファイルの解析 php bin/security-lint path/to/file.php # ディレクトリの解析(再帰的な関数解析が有効) php bin/security-lint app/ # 高重大度のみ表示 php bin/security-lint app/ -s high # JSON 形式で出力 php bin/security-lint app/ -f json -o report.json # 英語で出力 php bin/security-lint app/ -l en ``` --- ## 重大度レベル | レベル | 説明 | |--------|------| | **CRITICAL** | 即時対応が必要。直接的な攻撃が可能 | | **HIGH** | 早急な対応が必要。重大な脆弱性 | | **MEDIUM** | 計画的な対応が必要。潜在的なリスク | | **LOW** | 改善推奨。ベストプラクティス違反 | --- ## 制限事項 1. **静的解析の限界**: 実行時の値は判定できません 2. **動的なメソッド呼び出し**: `$obj->$method()` は追跡困難 3. **外部ライブラリ**: vendor 内のコードは解析対象外 4. **フレームワーク固有の機能**: 一部のLaravel固有パターンは検出できない場合があります 5. **偽陽性**: 安全なコードが危険と判定される場合があります --- ## バージョン - ドキュメント作成日: 2024 - 対応 PHP バージョン: 8.0+ - 対応 Laravel バージョン: 9.x, 10.x, 11.x