本サイトは、快適にご利用いただくためにクッキー(Cookie)を使用しております。
Cookieの使用に同意いただける場合は「同意する」ボタンを押してください。
なお本サイトのCookie使用については、「個人情報保護方針」をご覧ください。

最新情報

2023.11.06

Laravelのパラメータ取得方法

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();

FormRequestonly(), 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)の方法を使うと思いますが、そうであればこの問題は生じません。

プロフェッショナルサービス事業部
寺田健