作成動機
Webアプリケーションの脆弱性と対策について理解を深めたかったため、個人的に好きなLaravelでの対策をまとめた。
作成にあたって
作成する際に主にLaravelの公式ドキュメントとIPAの「安全なウェブサイトの作り方 改訂第7版」を参考にしながら作成した。
参考資料
また、対策例はあくまで一例です。
コードはGitHubにあげてます。
環境
- PHP 8.0.6
- Composer version 2.3.7
- Laravel Framework 9.52.12
- mysql Ver 8.0.29
前提
根本的解決と保険的対策
IPAでは脆弱性対策について、その性質を基に「根本的解決」と「保険的対策」の2つに分類している
根本的解決
「根本的解決」は「脆弱性を作りこまない実装」を実現する手法。
根本的解決を実施することにより、その脆弱性を狙った攻撃が無効化されることを期待できる。
保険的対策
「保険的対策」は「攻撃による影響を軽減する対策」。
脆弱性の原因そのものを無くすものではなく、被害範囲等を軽減するもの。
取り上げた脆弱性
- SQLインジェクション
- OSコマンドインジェクション
- ディレクトリトラバーサル
- XSS(クロスサイト・スクリプティング)
- CSRF(クロスサイト・リクエストフォージェリ)
SQLインジェクション
発生しうる脅威
- データベースに蓄積された非公開情報の閲覧
- 個人情報の漏洩 等
- データベースに蓄積された情報の改ざん、消去
- ウェブページの改ざん、パスワード変更、システム停止 等
- 認証回避による不正ログイン
- ログインした利用者に許可されているすべての操作を不正に行われる
- ストアプロシージャ等を利用したOSコマンドの実行
- システムの乗っ取り、他への攻撃の踏み台としての悪用
根本的解決
- SQL文の組み立ては全てプレースホルダで実装する
- SQL文の組み立てを文字列連結により行う場合は、エスケープ処理等を行うデータベースエンジンのAPIを用いて、SQL文のリテラルを正しく構成する
- ウェブアプリケーションに渡されるパラメータにSQL文を直接指定しない
保険的対策
- エラーメッセージをそのままブラウザに表示しない
- データベースアカウントに適切な権限を与える
Laravelでの対策
実例
このような簡易的なログインフォームがあるとする。
不適切な例
下記のようにwhereRaw
でSQL文を直書きしていると、' or 1 = 1 or ';
の様な値が入力された場合不正ログインに繋がる。
# sql文直書き
$exists = DB::table('unmeasured')->whereRaw('user_name = \'' . $name . '\'')->whereRaw('password = \'' . $passwd . '\'')->exists();
DBクエリログ
"query" => "select exists(select * from `unmeasured` where user_name = '' or 1 = 1 or ';' and password = '') as `exists`"
"bindings" => []
適切な例
- クエリビルダを使用する
Laravelクエリビルダは、PDOパラメーターバインディングを使用して、SQLインジェクション攻撃からアプリケーションを保護します。クエリバインディングとしてクエリビルダに渡たす文字列をクリーンアップやサニタイズする必要はありません。
DBの操作を下記のように行うことで、プリペアドステートメントが使われるようになる。
$obj = Remedied::select('password')->where('user_name', $name)->get();
DBのクエリログ
"query" => "select `password` from `remedieds` where `user_name` = ?"
"bindings" => array:1 [▼
0 => "hoge"
]
OSコマンドインジェクション
発生しうる脅威
- サーバー内のファイルの閲覧、改ざん、削除
- 重要情報の漏洩、設定ファイルの改ざん 等
- 不正なシステムの操作
- 意図しないOSのシャットダウン、ユーザーアカウントの追加、変更 等
- 不正なプログラムのダウンロード、実行
- ウイルス、ワーム、ボットなどへの感染、バックドアの設置 等
- 他のシステムへの攻撃の踏み台
- サービス不能攻撃、システム攻略のための調査、迷惑メールの送信 等
根本的解決
- シェルを起動できる言語機能の利用を避ける
- シェルを起動できる言語機能を利用する場合は、その引数を構成するすべての変数に対してチェックを行い、あらかじめ許可した処理のみ実行する
Laravelでの対策
実例
下記のようなフォームがあるとする。
不適切な例
外部入力フォームの値をそのままexec(), shell_exec(), system()等の関数の引数に入れている
$command = $request->get('command');
try {
$result = shell_exec($command);
$utf8 = iconv('Shift_JIS', 'UTF-8', $result);
} catch (\Exceptin $e) {
$result = 'エラー!';
}
powershell -Command "ping 8.8.8.8"
のような値を入力すると、そのままコマンドが実行される。
適切な例
validate
メソッド等を使用して入力値を適切にバリデーションする。
ディレクトリトラバーサル
発生しうる脅威
- サーバー内ファイルの閲覧、改ざん、削除
- 重要情報の漏洩
- 設定ファイル、データファイル、プログラムのソースコード等の改ざん、削除
根本的解決
- 外部からのパラメータでウェブサーバー内のファイル名を直接指定する実装を避ける
- ファイルを開く際は、固定のディレクトリを指定し、かつファイル名にディレクトリ名が含まれないようにする
保険的対策
- ウェブサーバー内のファイルへのアクセス権限の設定を正しく管理する
- ファイル名のチェックを行う
Laravelでの対策
下記のフォームでは直接プロジェクト内のファイル名を指定できるようになっている。
実例
不適切な例
入力値をそのままinclude
関数の引数に入れてファイルを取得している。
try {
include($path);
} catch (\Exception $e) {
$e_msg = '取得できませんでした';
return view('path_i', ['e_msg' => $e_msg]);
}
このフォームに../.env
のような値を入力すると、機密情報の漏洩につながる可能性がある。
適切な例
basename
関数を使用し、パスの最後にある名前の部分を返すようにする。
$path = basename($request->get('path'));
".."
のような値は除外される。
XSS(クロスサイト・スクリプティング)
発生しうる脅威
- 本物サイト上に偽のページが表示される
- 偽情報の流布による混乱
- フィッシング詐欺による重要情報の漏洩 等
- ブラウザが保存しているCookieを取得される
- CookieにセッションIDが格納されている場合、さらに利用者へのなりすましにつながる
- Cookieに個人情報などが格納されている場合、その情報が漏洩する
- 任意のCookieをブラウザに保存させられる
- セッションIDが利用者に送り込まれ、「セッションIDの固定化」攻撃に悪用される
根本的解決
- ウェブページに出力するすべての要素に対して、エスケープ処理を施す
- 入力されたHTMLテキストから構文解析を作成し、スクリプトを含まない必要な要素のみを抽出する
- HTTPレスポンスヘッダのContent-Typeフィールドに文字コード(charset)を指定する
保険的対策
- 入力値の内容チェックを行う
- 入力されたHTMLテキストから、スクリプトに該当する文字列を排除する
- Cookie情報の漏洩対策として、発行するCookieにHttpOnly属性を加え、TRACEメソッドを無効化する
Laravelでの対策
簡易的な掲示板でフォームの入力値がDBに保存され、表示されるようになっているページがある。
実例
不適切な例
入力値をBladeでエスケープ処理せずに表示する場合、<script>alert("XSS")</script>
のような値が投稿されるとスクリプトが動く。
デフォルトのBlade {{ }}文は、XSS攻撃を防ぐために、PHPのhtmlspecialchars関数を通して自動的に送信します。データをエスケープしたくない場合は、次の構文を使用します。
Hello, {!! $name !!}
Bladeで{!! !!}
とすることでエスケープ処理を回避できる
{!! $data->comment !!}
適切な例
- Bladeで
{{ }}
を使用しエスケープ処理を施す。
Bladeの{{ }}エコー文は、XSS攻撃を防ぐために、PHPのhtmlspecialchars関数を通して自動的に送信します。
{{ $data->comment }}
- e()ヘルパを使用する。
e関数は、PHPのhtmlspecialchars関数をdouble_encodeオプションにデフォルトでtrueを指定し、実行します。
CSRF(クロスサイト・リクエストフォージェリ)
発生しうる脅威
- ログイン後の利用者のみが利用可能なサービスの悪用
- 不正な送金、利用者が意図しない商品購入、利用者が意図しない退会処理 等
- ログイン後の利用者のみが編集可能な情報の改ざん、新規登録
- 各種設定の不正な変更(管理者画面、パスワード等)、掲示板への不適切な書き込み 等
根本的解決
- 処理を実行するページをPOSTメソッドでアクセスするようにし、その「hiddenパラメータ」に秘密情報が挿入されるよう、前のページを自動生成して、実行ページではその値が正しい場合のみ処理を実行する
- 処理を実行する直前のページで再度パスワードの入力を求め、実行ページでは、再度入力されたパスワードが正しい場合のみ処理を実行する
- Refererが正しいリンク元化を確認し、正しい場合のみ処理を実行する
保険的対策
- 重要な操作を行った際に、その旨を登録済みのメールアドレスに自動送信する
Laravelでの対策
実例
不適切な例
csrfトークン
を無効化している。
デフォルトではcsrfトークン
は必須になっているが、無効化することも可能。
場合により、一連のURIをCSRF保護から除外したいことが起きます。たとえば、Stripeを使用して支払いを処理し、そのWebhookシステムを利用している場合、StripeはどのCSRFトークンをルートへ送るのか認識していないため、Stripe WebフックハンドラルートをCSRF保護から除外する必要があります。
通常、これらの種類のルートはroutes/web.phpファイル中で、App\Providers\RouteServiceProviderがすべてのルートへ適用するwebミドルウェアグループの外側に配置する必要があります。ただし、VerifyCsrfTokenミドルウェアの$exceptプロパティにURIを追加してルートを除外することもできます。
protected $except = [
'http://127.0.0.1:8000/csrf/unmeasured/*',
];
csrfトークン
を使用せず処理されている場合、下記のようなサイトにアクセスすると意図せずユーザー情報などが変更されてしまう。
<form action="{{ route('post_csrf_rem_edit') }}" method="post" name="f1">
<input type="hidden" name="new_name" value="hacked">
<input type="hidden" name="new_email" value="hacked@hacked.com">
</form>
<script>
document.forms['f1'].submit();
</script>
適切な例
@csrf
をBladeのformに付け、csrfトークン
を用いる。
@csrf
はhiddenパラメータでトークンの値がFormに埋め込まれる。
Laravelではデフォルトでcsrfトークン
がない場合419エラーを返す。
Laravelは、アプリケーションによって管理されているアクティブなユーザーセッションごとにCSRF「トークン」を自動的に生成します。このトークンは、認証済みユーザーが実際にアプリケーションへリクエストを行っているユーザーであることを確認するために使用されます。このトークンはユーザーのセッションに保存され、セッションが再生成されるたびに変更されるため、悪意のあるアプリケーションはこのトークンへアクセスできません。
<form action="{{ route('post_csrf_rem_edit') }}" method="post">
@csrf
<p>ユーザー名</p>
<input type="text" name="new_name" value="{{ $name }}">
<p>メールアドレス</p>
<input type="text" name="new_email" value="{{ $email }}">
<p>パスワード</p>
<input type="password" name="password" required>
<br>
<button class="submit-button" type="submit">編集</button>
</form>
おまけ
Model関連のおまけ。
複数代入の脆弱性から保護するため下記のプロパティを使おう。
createメソッドを使用する前に、モデルクラスでfillableまたはguardedプロパティを指定する必要があります。すべてのEloquentモデルはデフォルトで複数代入の脆弱性から保護されているため、こうしたプロパティが必須なのです。
-
fillableを使用する
- 複数代入操作を行うときデフォルトで、$fillable配列に含まれない属性は黙って破棄されます。
- いわゆるホワイトリスト形式
-
guardedを使用する
- 入力させないカラムを指定
- いわゆるブラックリスト形式
参考資料