LaravelCollectiveのFormファサードを使用している場合にinputの値をGETパラメータから変更出来てしまう

タイトルの通り、LaravelCollectiveのFormファサードを使用してinputフィールドを作成している場合、nameと同じ値のGETパラメータをつけるとvalueの値を直接変更出来てしまうようなので調べてみた。

# 追記 (2018/07/06)
コメントで指摘いただきましたが、5.4、5.6(5.6.9以上）ではこの挙動はデフォルトではオフになっており、オプション扱いになったようですf。

以下のように `considerRequest` を `true` とすると、GETパラメータから値を変更出来るようになります。
(デフォルトでは `false`です）

```
{{ Form::considerRequest(true) }}
```

* [Make Request Optional by decadence · Pull Request #376 · LaravelCollective/html](https://github.com/LaravelCollective/html/pull/376)
* [Repopulating form from a get request not working on 5.6.9 · Issue #531 · LaravelCollective/html](https://github.com/LaravelCollective/html/issues/531)

このプルリクが5.4に対して出されており、マージ後、(その時点での）最新のもの(5.6.9)にマージされたため、5.4と5.6.9以降、にはこのオプションが適用されています。(5.5にはないので注意）
（ドキュメントにはこのオプションのことは書かれていないので注意が必要…。）

ですので、以下の説明は適用されていないバージョンに対しての話となりますのでご注意下さい。
最新のバージョンだと問題ありません。

# 再現
LaravelCollectiveを入れて(5.2以上)、以下のようにBladeに記述する。

```
{{ Form::text('test') }}
```

ページにアクセスすると以下のようなHTMLが生成される。

```html
<input name="test" type="text">
```

で、このページに以下のようにクエリをつけてアクセスする。

```
https://example.com/?test=hoge
```

すると`value`に`hoge`が入った状態でHTMLが生成される。

```html
<input name="test" type="text" value="hoge">
```

# 原因
LaravelCollectiveの仕様。
ドキュメントにも書いてある。

<https://github.com/LaravelCollective/docs/blob/5.6/html.md#form-model-binding>

>So, the priority looks like this:
>
>1. Session Flash Data (Old Input)
>2. Data From Current Request (via Request::input method)
>3. Explicitly Passed Value
>4. Model Attribute Data

以下のコードが該当箇所。

<https://github.com/LaravelCollective/html/blob/5.6/src/FormBuilder.php#L1263-L1265>

```php
$request = $this->request($name);
if (! is_null($request) && $name != '_method') {
    return $request;
}
```

5.2から導入されたようで、以下のプルリクで実装されたよう。
[Fill values by current Request by decadence · Pull Request #217 · LaravelCollective/html](https://github.com/LaravelCollective/html/pull/217)

# 懸念点

懸念点としては以下などがあると思う。

以下のstackoverflowでも書いている人がいた。
[laravel - Stop LaravelCollective Form Model Binding being overwritten by GET Variables? - Stack Overflow](https://stackoverflow.com/questions/50415265/stop-laravelcollective-form-model-binding-being-overwritten-by-get-variables)

## hidden値の書き換え
`Form::input`のファサード内に適用されているため、`hidden`フィールドにも上記仕様は適用される。
よって、getパラメータからhiddenのvalueを任意の値に設定出来てしまう。
すぐさまセキュリティのリスクになるわけではないけれど意図しない動作をしてしまう可能性がある。

## htmlspecialcharsによるexception
valueの値はHTMLを生成する際に`htmlspecialchars`にかけられるので、XSSが発生することはないのだけど、そのせいで配列を渡すとexceptionを吐いて落ちてしまう。
以下、value値を`e`で生成している箇所
<https://github.com/LaravelCollective/html/blob/5.6/src/HtmlBuilder.php#L463>

前述の例だと、以下のようなURLでアクセスするとページがexceptionを吐く。

```
https://example.com/?test[]=hoge
```

>htmlspecialchars() expects parameter 1 to be string, array given

# 代替案
フォーム部分のためにLaravelCollectiveを使用しているのなら、使用しない、という選択もありだと思う。
例えば、`Form::open()`のファサードはCSRF用のトークンも自動で生成してくれるので便利だが、Laravel5.6からは、Bladeに`@csrf`でトークンを生成してくれるディレクティブが追加されたりしている。
(5.3以降なら`csrf_field`ヘルパ関数でも生成出来る）

* [CSRF Protection - Laravel - The PHP Framework For Web Artisans](https://laravel.com/docs/5.6/csrf#csrf-introduction)
* [CSRF保護 5.6 Laravel](https://readouble.com/laravel/5.6/ja/csrf.html)
* [CSRF保護 5.5 Laravel](https://readouble.com/laravel/5.5/ja/csrf.html)

エラー時や確認ページから戻った際の前回の入力値 (Session Flash Data) は、`old()`で入れておけばよい。

```html
<input type="text" value="{{ old('test') }}">
```

`old`の第二引数にデフォルトの値も設定可能なので、既存のレコードの編集フォームも対応出来る。

<https://laravel.com/docs/5.6/helpers#method-old>

## ファサードの上書き
あまりよくはないと思うけど、ファサードの上書きで、該当の`request`からの取得箇所を消してしまう事も出来た。

参考 : [Best way to overwrite option method from Laravel 5 FormBuilder class?](https://laracasts.com/discuss/channels/general-discussion/best-way-to-overwrite-option-method-from-laravel-5-formbuilder-class)

どこでもよいが、例えば、`app/Service/Html/`以下に作成する。
上書き用に元の`HtmlServiceProvider`をextendsしたプロバイダクラスを作成し、カスタムの`FormBuilder`を読み込ませるようにする。

```app/Service/Html/HtmlServiceProvider.php
<?php

namespace App\Service\Html;

class HtmlServiceProvider extends \Collective\Html\HtmlServiceProvider
{
    protected function registerFormBuilder()
    {
        $this->app->singleton('form', function ($app) {
            $form = new FormBuilder($app['html'], $app['url'], $app['view'], $app['session.store']->token(), $app['request']);

            return $form->setSessionStore($app['session.store']);
        });
    }
}
```


```app/Service/Html/FormBuilder.php
<?php

namespace App\Service\Html;

use Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull;

class FormBuilder extends \Collective\Html\FormBuilder
{
    public function getValueAttribute($name, $value = null)
    {

        if (is_null($name)) {
            return $value;
        }

        $old = $this->old($name);

        if (! is_null($old) && $name !== '_method') {
            return $old;
        }

        if (function_exists('app')) {
            $hasNullMiddleware = app("Illuminate\Contracts\Http\Kernel")
                ->hasMiddleware(ConvertEmptyStringsToNull::class);

            if ($hasNullMiddleware
                && is_null($old)
                && is_null($value)
                && ! is_null($this->view->shared('errors'))
                && count($this->view->shared('errors')) > 0
            ) {
                return null;
            }
        }

		// ここに request から取得するコードがあったので削除

        if (! is_null($value)) {
            return $value;
        }

        if (isset($this->model)) {
            return $this->getModelValueAttribute($name);
        }
    }
}
```

あとは、`app.php`で`HtmlServiceProvider`を読み込んでいる箇所を変更。

```config/app.php
'providers' => [
	...
	//Collective\Html\HtmlServiceProvider::class,
    App\Service\Html\HtmlServiceProvider::class,
```

これであとは普通に使用すればrequestからは取得されなくなる。

# 感想
Laravel4.x系まではLaravelの中に組み込まれていたけれど、5.x系から分離されたコミュニティベースでの更新に移行されたよう。
便利ではあるけれど、使わないと実装出来ない、というほどのものでなければ使わないほうがむしろシンプルになってよいのかもしれない。

ちなみに、2018/7/4現在、LaravelCollectiveの公式サイトが証明書の期限が切れてしまっているよう。
<https://laravelcollective.com/>
GitHubにissueも上がってた。
[Website security cert expired. · Issue #546 · LaravelCollective/html](https://github.com/LaravelCollective/html/issues/546)