Initial commit: PHP/Laravel Security Linter v1.0.0
A static security analysis tool for PHP and Laravel applications with recursive taint analysis capabilities. Features: - Comprehensive vulnerability detection (XSS, SQL Injection, Command Injection, Path Traversal, CSRF, Authentication issues) - Recursive taint analysis across function calls - Blade template analysis with context-aware XSS detection - Smart escape detection and escape bypass detection - Syntax highlighting in terminal output - Multi-language support (Japanese/English) - Docker support for easy deployment - Multiple output formats (text, JSON, HTML, SARIF, Markdown) - CI/CD integration ready (GitHub Actions, GitLab CI) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
861
docs/DETECTION_RULES.md
Normal file
861
docs/DETECTION_RULES.md
Normal file
@@ -0,0 +1,861 @@
|
||||
# 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) !!}
|
||||
|
||||
{{-- 文字列リテラルとエスケープ値の連結 --}}
|
||||
{!! '<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 |
|
||||
|
||||
---
|
||||
|
||||
## 再帰的なテイント解析
|
||||
|
||||
リンターは、関数呼び出しを通じてユーザー入力(テイントデータ)がどのように伝播するかを追跡します。
|
||||
|
||||
### テイントソース(汚染源)
|
||||
|
||||
```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
|
||||
290
docs/QUICK_REFERENCE.md
Normal file
290
docs/QUICK_REFERENCE.md
Normal file
@@ -0,0 +1,290 @@
|
||||
# セキュリティリンター クイックリファレンス
|
||||
|
||||
## XSS 検出パターン
|
||||
|
||||
### Blade テンプレート
|
||||
|
||||
```
|
||||
重大度: CRITICAL
|
||||
├── <script>{!! $var !!}</script> # JS内の生出力
|
||||
└── onclick="{!! $var !!}" # イベントハンドラ内の生出力
|
||||
|
||||
重大度: HIGH
|
||||
├── {!! $var !!} # 生出力
|
||||
├── {!! func($var) !!} # 関数がエスケープしない場合
|
||||
├── {!! html_entity_decode(...) !!} # エスケープ破壊
|
||||
├── {!! '<script>...' . $var !!} # 危険なハードコードHTML
|
||||
├── onclick="{{ $var }}" # イベントハンドラ
|
||||
└── @include($var) # テンプレートインジェクション
|
||||
|
||||
重大度: MEDIUM
|
||||
├── <script>{{ $var }}</script> # JS コンテキスト
|
||||
├── href="{{ $var }}" # URL コンテキスト
|
||||
├── style="{{ $var }}" # CSS インジェクション
|
||||
└── <svg>{{ $var }}</svg> # SVG コンテキスト
|
||||
|
||||
重大度: LOW
|
||||
├── data-x={{ $var }} # 引用符なし属性
|
||||
└── @json($var) in <script> # JSON ディレクティブ
|
||||
```
|
||||
|
||||
### 安全なパターン(検出されない)
|
||||
|
||||
```blade
|
||||
{!! htmlspecialchars($var) !!} ✓ エスケープ関数
|
||||
{!! e($var) !!} ✓ Laravel ヘルパー
|
||||
{!! '<div>' . e($var) . '</div>' !!} ✓ 安全な連結
|
||||
{!! csrf_field() !!} ✓ 組み込み関数
|
||||
{!! $errors !!} ✓ Laravel 組み込み
|
||||
{!! Markdown::parse($var) !!} ✓ Markdown プロセッサ
|
||||
{!! safeFunc($var) !!} ✓ エスケープを返す関数
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## SQLインジェクション 検出パターン
|
||||
|
||||
```
|
||||
重大度: HIGH
|
||||
├── DB::raw("... $var ...")
|
||||
├── DB::select("... $var ...")
|
||||
├── ->whereRaw("... $var ...")
|
||||
├── ->orderByRaw($var)
|
||||
├── $pdo->query("... $var ...")
|
||||
├── $mysqli->query("... $var ...")
|
||||
└── "SELECT * FROM t WHERE id = $var"
|
||||
```
|
||||
|
||||
### SQLサニタイザー関数(これらは安全と判定)
|
||||
|
||||
```php
|
||||
intval($var), (int)$var ✓ 型キャスト
|
||||
floatval($var), (float)$var ✓ 型キャスト
|
||||
mysqli_real_escape_string($conn, $var) ✓ エスケープ
|
||||
$pdo->quote($var) ✓ PDO エスケープ
|
||||
filter_var($var, FILTER_VALIDATE_INT) ✓ 検証
|
||||
```
|
||||
|
||||
### SQLサニタイザー破壊パターン(危険)
|
||||
|
||||
```php
|
||||
stripslashes(mysqli_real_escape_string(...)) ✗
|
||||
urldecode(addslashes($var)) ✗
|
||||
html_entity_decode($pdo->quote($var)) ✗
|
||||
```
|
||||
|
||||
### 安全なパターン
|
||||
|
||||
```php
|
||||
DB::select('SELECT * FROM t WHERE id = ?', [$id]); ✓
|
||||
$query->where('column', '=', $value); ✓
|
||||
$query->whereRaw('col = ?', [$value]); ✓
|
||||
"SELECT * FROM t WHERE id = " . intval($id) ✓
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## コマンドインジェクション 検出パターン
|
||||
|
||||
```
|
||||
重大度: CRITICAL
|
||||
├── exec($cmd . $input)
|
||||
├── shell_exec($cmd . $input)
|
||||
├── system($cmd . $input)
|
||||
├── `$cmd $input`
|
||||
├── eval($code)
|
||||
├── include($userPath)
|
||||
└── preg_replace('/.../e', ...)
|
||||
|
||||
重大度: HIGH
|
||||
├── call_user_func($userCallback)
|
||||
├── Process::fromShellCommandline($cmd)
|
||||
└── Artisan::call($userCmd)
|
||||
```
|
||||
|
||||
### コマンドサニタイザー関数(これらは安全と判定)
|
||||
|
||||
```php
|
||||
escapeshellarg($arg) ✓ 引数エスケープ
|
||||
escapeshellcmd($cmd) ✓ コマンドエスケープ
|
||||
basename($path) ✓ ファイル名のみ
|
||||
intval($var) ✓ 型キャスト
|
||||
```
|
||||
|
||||
### コマンドサニタイザー破壊パターン(危険)
|
||||
|
||||
```php
|
||||
urldecode(escapeshellarg($var)) ✗
|
||||
str_replace("'", "", escapeshellarg($var)) ✗
|
||||
stripslashes(escapeshellcmd($var)) ✗
|
||||
```
|
||||
|
||||
### 安全なパターン
|
||||
|
||||
```php
|
||||
$process = new Process(['cmd', $arg1, $arg2]); ✓
|
||||
exec(escapeshellcmd($cmd) . ' ' . escapeshellarg($a)); ✓
|
||||
exec('ls -la ' . escapeshellarg($path)); ✓
|
||||
exec('kill ' . intval($pid)); ✓
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## パストラバーサル 検出パターン
|
||||
|
||||
```
|
||||
重大度: HIGH
|
||||
├── file_get_contents($userPath)
|
||||
├── fopen($userPath, 'r')
|
||||
├── unlink($userPath)
|
||||
├── move_uploaded_file($tmp, $userDest)
|
||||
└── response()->download($userPath)
|
||||
|
||||
重大度: MEDIUM
|
||||
├── Storage::get($userPath)
|
||||
└── Storage::put($userPath, $content)
|
||||
```
|
||||
|
||||
### パスサニタイザー関数(これらは安全と判定)
|
||||
|
||||
```php
|
||||
basename($path) ✓ ディレクトリ除去
|
||||
realpath($path) ✓ パス正規化
|
||||
pathinfo($path, PATHINFO_BASENAME) ✓ ファイル名抽出
|
||||
intval($id) ✓ 数値ID
|
||||
Str::random(40), Str::uuid() ✓ 安全なファイル名
|
||||
$file->hashName() ✓ ハッシュファイル名
|
||||
```
|
||||
|
||||
### 危険なパストラバーサルパターン
|
||||
|
||||
```
|
||||
.. ✗ ディレクトリトラバーサル
|
||||
../ ..\\ ✗ Unix/Windows
|
||||
%2e%2e ✗ URLエンコード
|
||||
%252e%252e ✗ ダブルエンコード
|
||||
```
|
||||
|
||||
### パスサニタイザー破壊パターン(危険)
|
||||
|
||||
```php
|
||||
urldecode(basename($var)) ✗ %2e%2e -> ..
|
||||
base64_decode($var) ✗ トラバーサル隠蔽
|
||||
$file->getClientOriginalName() ✗ ユーザー制御
|
||||
```
|
||||
|
||||
### 安全なパターン
|
||||
|
||||
```php
|
||||
$safePath = basename($userInput); ✓
|
||||
$realPath = realpath($path); ✓
|
||||
if (str_starts_with($realPath, $allowedDir)) { ... } ✓
|
||||
'/docs/' . intval($id) . '.txt' ✓
|
||||
$request->file('doc')->hashName() ✓
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 認証 検出パターン
|
||||
|
||||
```
|
||||
重大度: HIGH
|
||||
├── md5($password)
|
||||
├── sha1($password)
|
||||
├── $password = 'hardcoded'
|
||||
└── base64_encode($password)
|
||||
|
||||
重大度: MEDIUM
|
||||
├── $token == $userToken # タイミング攻撃
|
||||
└── password_hash($pw, ..., ['cost' => 4])
|
||||
```
|
||||
|
||||
### 安全なパターン
|
||||
|
||||
```php
|
||||
password_hash($pw, PASSWORD_ARGON2ID); ✓
|
||||
password_verify($input, $hash); ✓
|
||||
hash_equals($expected, $actual); ✓
|
||||
Hash::make($password); ✓
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CSRF/セッション 検出パターン
|
||||
|
||||
```
|
||||
重大度: HIGH
|
||||
└── <form method="POST"> without @csrf
|
||||
|
||||
重大度: MEDIUM
|
||||
├── session_start() without secure options
|
||||
├── Cookie without httponly
|
||||
└── session_regenerate_id() without true
|
||||
```
|
||||
|
||||
### 安全なパターン
|
||||
|
||||
```blade
|
||||
<form method="POST">
|
||||
@csrf ✓
|
||||
@method('PUT') ✓
|
||||
</form>
|
||||
```
|
||||
|
||||
```php
|
||||
session_start([
|
||||
'cookie_httponly' => true, ✓
|
||||
'cookie_secure' => true, ✓
|
||||
'cookie_samesite' => 'Strict', ✓
|
||||
]);
|
||||
session_regenerate_id(true); ✓
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 設定 検出パターン
|
||||
|
||||
```
|
||||
重大度: HIGH
|
||||
├── phpinfo()
|
||||
└── unserialize($data) without allowed_classes
|
||||
|
||||
重大度: MEDIUM
|
||||
├── dd($var), dump($var)
|
||||
├── var_dump($var), print_r($var)
|
||||
├── error_reporting(-1)
|
||||
├── ini_set('display_errors', '1')
|
||||
└── Log::info($password)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CLI 使用方法
|
||||
|
||||
```bash
|
||||
# 基本
|
||||
php bin/security-lint <path>
|
||||
|
||||
# オプション
|
||||
-f, --format 出力形式 (text|json|html|sarif|markdown)
|
||||
-s, --severity 最小重大度 (low|medium|high|critical)
|
||||
-o, --output ファイルに出力
|
||||
-l, --lang 言語 (ja|en)
|
||||
-e, --exclude 除外パターン
|
||||
-d, --recursive-depth 再帰深度 (default: 10)
|
||||
--verbose 詳細表示
|
||||
|
||||
# 例
|
||||
php bin/security-lint app/ -s high -f json -o report.json
|
||||
php bin/security-lint resources/views/ -l en
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 終了コード
|
||||
|
||||
| コード | 意味 |
|
||||
|--------|------|
|
||||
| 0 | 問題なし |
|
||||
| 1 | 中/低重大度の問題あり |
|
||||
| 2 | クリティカル/高重大度の問題あり |
|
||||
Reference in New Issue
Block a user