- Add section 8 to DETECTION_RULES.md covering: - Mass Assignment detection patterns - Raw SQL injection detection - CSRF protection checks - File upload validation rules - Route authentication middleware - Rate limiting detection - Update README.md with Laravel-specific security in detectable vulnerabilities section (ja/en) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1098 lines
31 KiB
Markdown
1098 lines
31 KiB
Markdown
# 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. **偽陽性**: 安全なコードが危険と判定される場合があります
|
||
|
||
---
|
||
|
||
## バージョン
|
||
|
||
- ドキュメント作成日: 2024
|
||
- 対応 PHP バージョン: 8.0+
|
||
- 対応 Laravel バージョン: 9.x, 10.x, 11.x
|