2016-11-07 · PHP Laravel

Laravelのuniqueバリデーションに潜むセキュリティリスクについて

このエントリーをはてなブックマークに追加

既存の問題

仕事の都合でまたPHPを始めました。

そのPHPで近年ナイスでホットなフレームワークがLaravel 5ということで、Laravelリファレンスを片手に研究しているのですが、uniqueバリデーションがレコード更新時にエラーを起こす問題日本語解説)にぶつかりました。

要するに、ユニークなカラムをフォームで編集せずそのまま送信した場合、自分自身のレコードをunique判定に掛けてしまってバリデーションに引っかかってしまうというのです。ヒエ〜!

色々なフォーラムを漁った結果、これに対する定石は、uniqueバリデータに自身のレコードのidを指定することでした。

public function rules() {
    return [
        'email' => [
            'required',        
            'unique:users,email,' . $this->id // ここにレコードのIDを指定する
        ]
    ]
}

Laravel 5.3.18からはignoreルールが搭載され、表記がいい感じになっています。

use Illuminate\Validation\Rule;

public function rules() {
    return [
        'email' => [
            'required',        
            Rule::unique('users')->ignore($this->id),
        ]
    ]
}

しかし、これらのコードは実装によっては脆弱性を抱える場合があります。

検証

先ほどのQiitaの記事のカスタムリクエストを使用した場合とほぼ同一です。

ルーティングファイル(抜粋)

// routes/web.php

Route::resource("hoges","HogeController");

コントローラ(update部分のみ抜粋)

// app/Http/Controllers/HogeController.php

public function update(HogeRequest $request, $id)
{
    $hoge = Hoge::findOrFail($id);

    $hoge->uniq = $request->input("uniq");

    $hoge->save();

    return redirect()->route('hoges.index')->with('message', 'Item updated successfully.');
}

フォームのHTML(抜粋)

<!-- resources/views/hoges/edit.blade.php -->

<form action="{{ route('hoges.update', $hoge->id) }}" method="POST">
    <input type="hidden" name="_method" value="PUT"/>
    <input type="hidden" name="_token" value="{{ csrf_token() }}"/>
    <input type="hidden" name="id" value="{{ $hoge->id }}"/>

    <label>Uniq</label>
    <input type="text" name="uniq" value="{{ is_null(old("uniq")) ? $hoge->uniq : old("uniq") }}"/>
    <button type="submit">Save</button>
</form>

カスタムリクエスト(ここでバリデーションを行います)

// app/Http/Requests/HogeRequest.php
namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;

class HogeRequest extends FormRequest
{
    public function rules()
    {
        return [
            'uniq' => [
                'required',
                Rule::unique('hoges')->ignore($this->id),
            ]
        ];
    }

    public function authorize()
    {
        return true;
    }
}

いかにも自然で問題なく見えますが、これは脆弱なコードです

上記の検証コードによって実装された以下のページ

ID#2の「piyopiyo」は「hogehoge」に変更することができません。

エラー。

なぜなら「hogehoge」はID#1のデータで既に使われており、uniqueバリデータに守られているためです。

さて、ここでform内のhiddenタグを一部書き換えてみましょう。

結果

ギエピー! uniqueなはずのhogehogeとhogehogeが被ってしまいました。

原因

カスタムリクエストの中にある$this->idはフォームのhiddenタグから取ってくるのに対して、実際にオブジェクトを更新する際に使う$idはURLから参照するためです。(バリデーションを行なうべきオブジェクトにバリデーションが行われていない状態になっています)

よって、フォーム内のidを改竄されてしまうと、uniqueバリデーションをすり抜けて好きな値を設定できてしまいます。

対策

以下の通りです。

  • カスタムリクエストのrules()内でidを引っ張る際、$this->idは使わない。$this->get('id')も同様。代わりに$this->segment(2)などでURLから取る。
  • もしくは、コントローラの引数$idを使わない。代わりに$request->input("id")を使う。(どちらかに統一する)
  • そもそもhiddenタグでidを指定しない。
  • データを保存する際にも正当性を検証する。というかデータベースでuniqueを保証する。
    public function rules()
    {
        return [
            'uniq' => [
                'required',
                Rule::unique('hoges')->ignore($this->segment(2)),
            ]
        ];
    }

ちょっと考えれば簡単なことですが、なんだか紛らわしい構文してるなぁ、という話でした。


前の記事: Halloween & Helloween