本サイトは、快適にご利用いただくためにクッキー(Cookie)を使用しております。
Cookieの使用に同意いただける場合は「同意する」ボタンを押してください。
なお本サイトのCookie使用については、「個人情報保護方針」をご覧ください。
MBSDの寺田です。普段はWebアプリケーションスキャナの開発をしています。
今回は、Laravelにおいてリクエストパラメータをどう取得すべきか?について書きます。Laravelにおいてややこしいのは、$request->get()
や$request->input()
など、パラメータを取得する手段が多くあることです。取得の方法によってはバリデーションの回避などの問題になりうる、というのが今回のテーマです。
下記の内容はLaravel 10系(10.17.0, 10.29.0)の動作をもとにしています。
失敗例
まずは分かりやすい例です。下のコードの何が悪いか分かるでしょうか?
Route::post('/bad1', function(Request $request) {
// パラメータのバリデーション(英数文字列のみ許可)
$request->validate(['foo' => 'string|regex:/^[0-9a-z]+$/D']);
// バリデーション済みのパラメータを取得して何かする
$foo = $request->get('foo');
var_dump($foo);
});
正常系では、リクエストボディのパラメータfoo
がバリデートされ、バリデートOK(英数文字列)の値のみがvar_dump()
されます。しかし、以下のようにクエリストリングとボディの両方にパラメータfoo
を付けたリクエストを送るとどうなるでしょうか。
POST /bad1?foo=@@@@ HTTP/1.1
Host: example.jp
Content-Type: application/x-www-form-urlencoded
Content-Length: …
foo=test&_token=…
PHPコードのvar_dump()
により出力されるのはstring(4) "@@@@"
という値です。こうなる理由は以下です。
- バリデーションの対象はボディ。
(クエリストリングとボディに同名のパラメータがある時は、ボディが優先) $request->get()
はクエリストリングの値を返す。
(クエリストリングとボディに同名のパラメータがある時は、クエリストリングが優先)
結果として、バリデーション後にパラメータを利用する箇所では、「英数文字列のみを許可」というバリデーションルールを満たさない値(@@@@
)を掴んでしまいます。
Laravelのアプリ開発において$request->get()
を使うことは少ないと思われるかもしれませんが、筆者は上記のようなコードを一度ならず見たことがあります。パラメータをgetする感じがするからかもしれません。
参考まで、パラメータの取得/検証処理の概要を以下にまとめます。
※ $A + $B … $Aが優先される。例えば「$_GET + $_POST」では$_GETが優先。
$request->query(…) $_GET
$request->get(…) $_GET + $_POST // Symfony側のget()そのまま
$request->input(…) $_POST + $_GET
$request->all(…) $_FILES + $_POST + $_GET
$request->only(…) all() // except()も同じ
$request->integer(…) input() // boolean()なども同じ
$request->name all() + Routeパラメータ // 動的プロパティ
request(…) all() + Routeパラメータ // helper関数
$request->validate(…) all()が対象
どうすればよかったのか
バリデーション処理と、その後にリクエストパラメータを使用する処理で、同じ値を参照するようにします。上記のようなコードであれば、get()
の代わりにall()
を使うということになりますが、通常はinput()
、helper関数、動的プロパティでも上のような問題は生じないはずです。
余談ですが、動的プロパティには、server
, query
, files
, attributes
などの名前のリクエストパラメータが取得できないという制約があります。これらは組み込みのpublicプロパティとしてsymfony側で定義されており、名前が衝突するからです。
また、以下のようにするとバリデーション済みの値を連想配列で取得できます。
$validated = $request->validate(['x' => 'string|regex:/…/');
バリデーション後に何らかの処理を行う時に$validated
の値を使えば、上記のような問題は生じません。ちなみに、Laravelのドキュメントにもvalidate()
の戻り値を利用する(ように見える)コードが載っています。
FormRequest
クラスを使用している場合は、以下で同じことができます。
$validated = $request->validated();
FormRequest
でonly()
, except()
と組合わせた例はドキュメントを参照。
いずれの方法にせよ、パラメータ値の取得元を一貫させるということになります。
署名付きURL
Laravelの署名付きURLは、URLを改竄から保護するためのものです。ドキュメントにはRouteパラメータ(URLパスに埋め込むパラメータ)を保護する例しか載っていませんが、クエリストリングも保護できます。
以下は、署名付きURLを使用してクエリストリングを保護しているコードの例です。
// URL: http://example.jp/badsigned/2?bar=123&signature=8ad8d354…
Route::any('/badsigned/{post}', function(string $post, Request $request) {
// URLの署名を検証する
if (!$request->hasValidSignature()) {
abort(401);
}
// 署名検証済みのパラメータを取得して何かする
$bar = $request->bar;
var_dump($bar);
})->name('badsigned');
上のコードは、署名を検証してOKだった場合にのみパラメータbar
の値を出力していますが、何が問題でしょうか?
以下は、正常なリクエストと、署名検証をバイパスするための攻撃リクエストです。
【正常】
GET /badsigned/2?bar=123&signature=8ad8d354… HTTP/1.1
Host: example.jp
【攻撃】
POST /badsigned/2?bar=123&signature=8ad8d354… HTTP/1.1
Host: example.jp
Content-Type: application/x-www-form-urlencoded
Content-Length: …
bar=@@@@&_method=get
攻撃リクエストを送信すると、ボディの値であるstring(4) "@@@@"
が応答に出力されます。このような結果になるのは、前述のとおり$request->bar
ではボディの値が優先されるためです。
当然ですが、署名付きURLで検証されるのはURLだけです。検証されたURLのクエリストリングを取得する時は、$request->query()
を使う必要があります。要は、前述のとおり、検証時と使用時において「パラメータ値の取得元を一貫させる」必要があるということです。
Routeパラメータの取得
上のとおり、Routeパラメータも$request->name
などから取得できます。
Route::post('/bad1a/{baz}', function(Request $request, string $baz) {
var_dump($baz); // (1) … OK
var_dump($request->route('baz')); // (2) … OK
var_dump($request->baz); // (3) … NG
})->where('baz','[0-9a-z]+');
しかし、動的プロパティを利用した(3)の方法だと、クエリストリングやボディに同名のパラメータがあればその値を掴んでしまい、結果としてwhere()
のバリデーションが効かなくなります。通常は(1),(2)の方法を使うと思いますが、そうであればこの問題は生じません。
おすすめ記事