Files
php-security-linter/docs/DETECTION_RULES.md
Yutaka Kurosaki 208227b77e Prepare for OSS release v0.0.1
Version updates:
- Set version to 0.0.1 across all files
- Update CLI banner, SARIF output, and documentation

New files:
- LICENSE: MIT license
- CHANGELOG.md: Initial changelog with all features
- CONTRIBUTING.md: Contribution guidelines

composer.json enhancements:
- Add version, keywords, homepage, support URLs
- Add authors section
- Add require-dev for PHPUnit

README.md updates:
- Update repository URLs to security-linter/php-laravel
- Update Docker image references

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 11:57:18 +09:00

1098 lines
31 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# PHP/Laravel セキュリティリンター 検出ルール一覧
このドキュメントでは、セキュリティリンターが検出できる脆弱性パターンを詳細に説明します。
---
## 目次
1. [XSS (クロスサイトスクリプティング)](#1-xss-クロスサイトスクリプティング)
2. [SQLインジェクション](#2-sqlインジェクション)
3. [コマンドインジェクション](#3-コマンドインジェクション)
4. [パストラバーサル](#4-パストラバーサル)
5. [認証セキュリティ](#5-認証セキュリティ)
6. [CSRF/セッションセキュリティ](#6-csrfセッションセキュリティ)
7. [設定セキュリティ](#7-設定セキュリティ)
8. [Laravel特有のセキュリティ](#8-laravel特有のセキュリティ)
---
## 1. XSS (クロスサイトスクリプティング)
### 1.1 Blade テンプレートの生出力
| パターン | 重大度 | 説明 |
|---------|--------|------|
| `{!! $var !!}` | HIGH | エスケープされていない変数の出力 |
| `{!! $array['key'] !!}` | HIGH | 配列アクセスの生出力 |
| `{!! $obj->prop !!}` | HIGH | プロパティアクセスの生出力 |
**安全と判定されるパターン:**
```blade
{{-- エスケープ関数でラップされている場合 --}}
{!! htmlspecialchars($var) !!}
{!! e($var) !!}
{!! htmlentities($var) !!}
{!! strip_tags($html) !!}
{{-- 文字列リテラルとエスケープ値の連結 --}}
{!! '<div>' . htmlspecialchars($name) . '</div>' !!}
{{-- 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 を検出します。
| 検出パターン | 説明 |
|-------------|------|
| `<script>` | スクリプトタグ |
| `<iframe>` | iframe タグ |
| `<object>`, `<embed>` | プラグインタグ |
| `<meta>`, `<link>`, `<base>` | メタ情報タグ |
| `<form>` | フォームタグ(フィッシング) |
| `onclick=`, `onerror=` 等 | イベントハンドラ |
| `javascript:` | JavaScript プロトコル |
| `vbscript:` | VBScript プロトコル |
| `data:...;base64` | Data URL |
**検出例:**
```php
// 危険: ハードコードされた script タグ
function badFormatter($input) {
return '<script>alert("XSS")</script>' . htmlspecialchars($input);
}
// 安全: 危険なタグを含まない
function safeFormatter($input) {
return '<div class="wrapper">' . htmlspecialchars($input) . '</div>';
}
```
### 1.4 JavaScript コンテキスト
| パターン | 重大度 | 説明 |
|---------|--------|------|
| `<script>{{ $var }}</script>` | MEDIUM | script 内の Blade 出力 |
| `<script>{!! $var !!}</script>` | CRITICAL | script 内の生出力 |
| `@json($var)` in `<script>` | LOW | JSON ディレクティブの使用 |
**推奨される対策:**
```blade
{{-- 推奨: Js::from() を使用 --}}
<script>
var data = {{ Js::from($data) }};
</script>
```
### 1.5 URL コンテキスト
`javascript:` URL によるXSSを検出します。
| パターン | 重大度 |
|---------|--------|
| `href="{{ $url }}"` | MEDIUM |
| `src="{{ $url }}"` | MEDIUM |
| `action="{{ $url }}"` | MEDIUM |
| `formaction="{{ $url }}"` | MEDIUM |
**安全と判定されるパターン:**
```blade
{{-- route() や url() ヘルパーは安全 --}}
<a href="{{ route('home') }}">Home</a>
<a href="{{ url('/about') }}">About</a>
<form action="{{ action('Controller@method') }}">
```
### 1.6 イベントハンドラ
| パターン | 重大度 |
|---------|--------|
| `onclick="{{ $var }}"` | HIGH |
| `onerror="{{ $var }}"` | HIGH |
| `onload="{{ $var }}"` | HIGH |
| `onclick="{!! $var !!}"` | CRITICAL |
**推奨される対策:**
```blade
{{-- 危険 --}}
<button onclick="doAction('{{ $action }}')">Click</button>
{{-- 推奨: data 属性を使用 --}}
<button data-action="{{ $action }}" class="js-action">Click</button>
<script>
document.querySelector('.js-action').addEventListener('click', function() {
doAction(this.dataset.action);
});
</script>
```
### 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 コンテキスト
| パターン | 重大度 |
|---------|--------|
| `<svg>{{ $var }}</svg>` | 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 トークン
| パターン | 重大度 |
|---------|--------|
| `<form method="POST">` 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 |
---
## 8. Laravel特有のセキュリティ
Laravel フレームワーク固有の脆弱性パターンを検出します。
### 8.1 Mass Assignment一括代入
Eloquent モデルで `$fillable` または `$guarded` プロパティが定義されていない場合を検出します。
| パターン | 重大度 |
|---------|--------|
| Model without `$fillable` or `$guarded` | HIGH |
| `Model::create($request->all())` | HIGH |
| `$model->fill($request->all())` | HIGH |
| `$model->update($request->all())` | HIGH |
**脆弱なコード:**
```php
// 危険: $fillable/$guarded がない
class User extends Model
{
// Mass Assignment 脆弱性
}
// 危険: $request->all() を直接使用
User::create($request->all());
$user->update($request->all());
```
**安全なコード:**
```php
// $fillable を定義
class User extends Model
{
protected $fillable = ['name', 'email'];
}
// または $guarded を定義
class User extends Model
{
protected $guarded = ['id', 'is_admin'];
}
// 特定のフィールドのみ使用
User::create($request->only(['name', 'email']));
$user->update($request->validated());
```
### 8.2 Raw SQL インジェクション
Laravel のクエリビルダーで生SQLを使用し、パラメータバインディングがない場合を検出します。
| パターン | 重大度 |
|---------|--------|
| `DB::raw($variable)` | HIGH |
| `whereRaw($sql)` (バインディングなし) | HIGH |
| `selectRaw($sql)` (バインディングなし) | HIGH |
| `orderByRaw($sql)` (バインディングなし) | HIGH |
| `havingRaw($sql)` (バインディングなし) | HIGH |
| `groupByRaw($sql)` (バインディングなし) | HIGH |
**脆弱なコード:**
```php
// 危険: 変数を含む DB::raw
$column = $request->input('column');
DB::table('users')->select(DB::raw($column))->get();
// 危険: バインディングなしの whereRaw
$name = $request->input('name');
User::whereRaw("name = '$name'")->get();
// 危険: バインディングなしの orderByRaw
$order = $request->input('order');
User::orderByRaw($order)->get();
```
**安全なコード:**
```php
// パラメータバインディングを使用
User::whereRaw('name = ?', [$name])->get();
User::orderByRaw('FIELD(status, ?, ?, ?)', ['active', 'pending', 'inactive'])->get();
// クエリビルダーを使用(推奨)
User::where('name', $name)->get();
User::orderBy($allowedColumns[$request->input('column')])->get();
```
### 8.3 CSRF 保護
POST/PUT/PATCH/DELETE メソッドのフォームに CSRF トークンがない場合を検出します。
| パターン | 重大度 |
|---------|--------|
| `<form method="POST">` without `@csrf` | HIGH |
| `<form method="post">` without `csrf_field()` | HIGH |
| `<form method="PUT">` without CSRF | HIGH |
| `<form method="DELETE">` without CSRF | HIGH |
**脆弱なコード:**
```blade
{{-- 危険: @csrf がない --}}
<form method="POST" action="/submit">
<input type="text" name="data">
<button type="submit">送信</button>
</form>
```
**安全なコード:**
```blade
{{-- @csrf ディレクティブを使用 --}}
<form method="POST" action="/submit">
@csrf
<input type="text" name="data">
<button type="submit">送信</button>
</form>
{{-- または csrf_field() ヘルパー --}}
<form method="POST" action="/submit">
{{ csrf_field() }}
<input type="text" name="data">
<button type="submit">送信</button>
</form>
```
### 8.4 ファイルアップロード検証
ファイルアップロードで `extensions` ルールのみを使用し、`mimes` または `mimetypes` ルールがない場合を検出します。拡張子のみのチェックは簡単にバイパスできます。
| パターン | 重大度 |
|---------|--------|
| `'file' => 'extensions:jpg,png'` (mimes なし) | MEDIUM |
| `'file' => ['extensions:jpg', 'file']` (mimes なし) | MEDIUM |
**脆弱なコード:**
```php
// 危険: 拡張子のみで検証
public function rules()
{
return [
'avatar' => ['required', 'file', 'extensions:jpg,png'],
'document' => 'file|extensions:pdf,doc',
];
}
```
**安全なコード:**
```php
// mimes または mimetypes を併用
public function rules()
{
return [
// mimes を使用(推奨)
'avatar' => ['required', 'file', 'mimes:jpeg,png'],
// extensions と mimes を両方使用
'document' => ['file', 'extensions:pdf,doc', 'mimes:pdf,msword'],
// mimetypes を使用
'video' => ['file', 'mimetypes:video/mp4,video/mpeg'],
];
}
```
### 8.5 ルート認証ミドルウェア
センシティブなルートに `auth` ミドルウェアがない場合を検出します。
| パターン | 重大度 |
|---------|--------|
| `Route::*('/admin/*')` without auth | LOW |
| `Route::*('/dashboard/*')` without auth | LOW |
| `Route::*('/user/*')` without auth | LOW |
| `Route::*('/account/*')` without auth | LOW |
| `Route::*('/settings/*')` without auth | LOW |
| `Route::*('/profile/*')` without auth | LOW |
**検出対象のセンシティブなルートパターン:**
- `/admin/`, `/dashboard/`, `/user/`, `/users/`
- `/account/`, `/settings/`, `/profile/`
- `/manage/`, `/management/`, `/billing/`
**脆弱なコード:**
```php
// 危険: 認証なしで管理画面にアクセス可能
Route::get('/admin/dashboard', [AdminController::class, 'index']);
Route::get('/users/manage', [UserController::class, 'manage']);
```
**安全なコード:**
```php
// ルートにミドルウェアを追加
Route::get('/admin/dashboard', [AdminController::class, 'index'])
->middleware('auth');
// グループでミドルウェアを適用(推奨)
Route::middleware(['auth'])->group(function () {
Route::get('/admin/dashboard', [AdminController::class, 'index']);
Route::get('/users/manage', [UserController::class, 'manage']);
});
```
### 8.6 レート制限
認証関連のルートに `throttle` ミドルウェアがない場合を検出します。ブルートフォース攻撃を防ぐために重要です。
| パターン | 重大度 |
|---------|--------|
| `Route::post('/login')` without throttle | LOW |
| `Route::post('/register')` without throttle | LOW |
| `Route::post('/password/reset')` without throttle | LOW |
| `Route::post('/password/email')` without throttle | LOW |
| `Route::post('/forgot-password')` without throttle | LOW |
| `Route::post('/2fa/*')` without throttle | LOW |
**脆弱なコード:**
```php
// 危険: レート制限なしでブルートフォース可能
Route::post('/login', [AuthController::class, 'login']);
Route::post('/password/reset', [PasswordController::class, 'reset']);
```
**安全なコード:**
```php
// throttle ミドルウェアを追加
Route::post('/login', [AuthController::class, 'login'])
->middleware('throttle:5,1'); // 1分間に5回まで
// グループでレート制限を適用
Route::middleware(['throttle:60,1'])->group(function () {
Route::post('/login', [AuthController::class, 'login']);
Route::post('/password/reset', [PasswordController::class, 'reset']);
});
```
---
## 再帰的なテイント解析
リンターは、関数呼び出しを通じてユーザー入力(テイントデータ)がどのように伝播するかを追跡します。
### テイントソース(汚染源)
```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. **偽陽性**: 安全なコードが危険と判定される場合があります
---
## バージョン
- バージョン: 0.0.1
- 対応 PHP バージョン: 8.1+
- 対応 Laravel バージョン: 9.x, 10.x, 11.x