LoginSignup
43
32

More than 3 years have passed since last update.

Laravelのemailバリデーション

Last updated at Posted at 2020-11-13

Laravelデフォルトのバリデーションルール「email」を使用したWEBサービスを作成したところ、バリデーション不足を指摘された。

環境
Laravel 5.8.38

問題点

ユーザー登録画面にて、test@example.comあいうえおといった、@以降にひらがなを含むメールアドレスを登録できてしまう。

RegisterController.php
protected function validator(array $data)
{
    return Validator::make($data, [
        'name' => ['required', 'string', 'max:100'],
        'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
        'password' => ['required', new AlphaNumHalf, 'min:8', 'confirmed'],
    ]);
}

スクリーンショット 2020-11-11 5.43.58.png

実際に登録されたメールアドレス
test@example.xn--com-m63bkmoq

まじか・・・

結論

'email'
 ↓
'email:strict,dns,spoof'
とりあえずこれ。dnsによりドメインが存在するアドレスのみバリデーションを通るので、意味不明な日本語を入れたらまず通らないようにはできると思う。
が、本当にこれが正しいのか分からないので、色々調べてみる。

何が起きているのか

Laravelの公式ドキュメントによれば、

email
フィールドがメールアドレスとして正しいことをバリデートします。内部でこのバリデーションルールはメールアドレスの検証にegulias/email-validatorパッケージを使用しています。デフォルトではRFCValidationバリデータが適用されますが、他のバリデーションスタイルも適用可能です。
適用可能なバリデーションスタイルは、次の通りです。
・rfc: RFCValidation
・strict: NoRFCWarningsValidation
・dns: DNSCheckValidation
・spoof: SpoofCheckValidation
・filter: FilterEmailValidation

emailバリデーションにも種類があるらしい。今更知った。デフォルトではRFCValidationが適用されるとのこと。
それぞれの詳細は下記サイトが参考になった。
【Laravel 5.8.33】メールアドレスの新バリデーション・実例全5件!

では、他のバリデーションスタイルでtest@example.comあいうえおを登録しようとするとどうなるか。結果はこちら。

バリデーションスタイル 結果
rfc test@example.xn--com-m63bkmoq
strict test@example.xn--com-m63bkmoq
dns 登録できない
spoof test@example.xn--com-m63bkmoq
filter test@example.xn--com-m63bkmoq

それぞれのバリデーションの詳細をみるとそりゃそうだよな、という感じ。

xn--com-m63bkmoqとはなんなのか

調べてみると、Punycodeという符号化方式で、comあいうえおの部分がアルファベット、数字、ハイフンのみの文字列xn--com-m63bkmoqに変換されているようだ。

ドメイン名として Punycode を使用する際は、ピリオド(.)で区切られたドメイン名の階層レベルごとにプレフィックスとして「xn--」を使用し、エンコードされた文字列を続ける。大文字と小文字は区別されない。

可読なドメイン名 Punycodeでのドメイン名
ドメイン名例.jp xn--eckwd4c7cu47r2wf.jp
ウィキペディア.ドメイン名例.jp xn--cckbak0byl6e.xn--eckwd4c7cu47r2wf.jp
可愛いね.そうでもないよ xn--n8j5d625jn9k.xn--n8jd2ewbp7lub

引用:Punycode

可愛いね.そうでもないよは笑う。

また、試しに日本語JPドメイン名のPunycode変換・逆変換にてcomあいうえおと入力してみると、やはりxn--com-m63bkmoqに変換された。

スクリーンショット 2020-11-11 13.02.43.png

カスタムバリデーションで正規表現を使ってみる

正規表現を使ったカスタムバリデーションを試そうともしたが、Punycodeでアルファベット、数字、ハイフンのみの文字列に変換されてしまうのならば、RFCValidation等と同じ結果になってしまうのではないだろうか?

念のため試してみる。
emailの正規表現は難しいらしく、どれを使えば良いのか・・・という感じだが、こちらの記事を参考にさせていただいた。
メールアドレスを表す現実的な正規表現

自作のバリデーションルール

MyEmailRegex.php
public function passes($attribute, $value)
{
    return preg_match("/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/", $value);
}
RegisterController.php
use App\Rules\AlphaNumHalf;

protected function validator(array $data)
{
    return Validator::make($data, [
        'name' => ['required', 'string', 'max:100'],
        'email' => ['required', 'string', new MyEmailRegex, 'max:255', 'unique:users'],
        'password' => ['required', new AlphaNumHalf, 'min:8', 'confirmed'],
    ]);
}

この正規表現の意味は、「何かしらの文字@英数字か"-".英数字か"-"」となる。
全角文字がPunycodeでアルファベット、数字、ハイフンのみの文字列に変換されてからバリデーションを通っていそうだが、そうであればやっぱり正規表現で引っ掛けるのは難しいのではないか。

test@example.comあいうえおを登録してみると、やっぱりバリデーションに引っかからずtest@example.xn--com-m63bkmoqと登録された。

このようなバリデーションは無難であると先の記事には書いてあるが、それなら僕の受けた指摘は一体なんなのだと釈然としない。

strict,dns,spoofを併用する

Laravelが用意してくれているバリデーション

  • strict: NoRFCWarningsValidation
  • dns: DNSCheckValidation
  • spoof: SpoofCheckValidation

は、併用が可能。

RegisterController.php
protected function validator(array $data)
{
    return Validator::make($data, [
        'name' => ['required', 'string', 'max:100'],
        'email' => ['required', 'string', 'email:strict,dns,spoof', 'max:255', 'unique:users'],
        'password' => ['required', new AlphaNumHalf, 'min:8', 'confirmed'],
    ]);
}

なぜ併用するかというと、
strictだけだと先に述べたようにtest@example.comあいうえおが通ってしまう。
dnsだけだと.test..@gmail.com等が通ってしまう。
※RFCでは@の前に.をつけること等が禁止されている。

strictでRFCに違反するアドレスがはじかれ、
dnsでドメインが有効でないアドレスがはじかれる。
ついでにspoofでなりすましメールもはじかれる。

ドメイン部分に日本語を入力し、Punycodeで変換されたものが実際に有効なドメインでなければ(そんなドメインがあるのか分からないが)、日本語を入力してもしっかりバリデーションしてくれるようになった。

結論

Laravelのemailのバリデーションについて、デフォルトのままemail(email:rfc)では不十分な場合がある。その場合は、email:dns等、他のバリデーションも併用すると良さそう。

ただ、バリデーションの種類云々と同じくらい大事なのが、「どの程度のバリデーションでサービスの依頼主は満足するか?」を、質問なり打ち合わせでしっかり確認しておくことだと感じた。(僕は何も確認せずに納品してしまったので)

記事の内容等に間違いがありましたら、申し訳ございません。

おまけ

vendor/laravel/framework/src/Illuminate/Validation/Concerns/ValidatesAttributes.php
public function validateEmail($attribute, $value, $parameters)
{
    if (! is_string($value) && ! (is_object($value) && method_exists($value, '__toString'))) {
        return false;
    }

    $validations = collect($parameters)
        ->unique()
        ->map(function ($validation) {
            if ($validation === 'rfc') {
                return new RFCValidation();
            } elseif ($validation === 'strict') {
                return new NoRFCWarningsValidation();
            } elseif ($validation === 'dns') {
                return new DNSCheckValidation();
            } elseif ($validation === 'spoof') {
                return new SpoofCheckValidation();
            } elseif ($validation === 'filter') {
                return new FilterEmailValidation();
            }
        })
        ->values()
        ->all() ?: [new RFCValidation()];

    return (new EmailValidator)->isValid($value, new MultipleValidationWithAnd($validations));
}
vendor/laravel/framework/src/Illuminate/Validation/Concerns/FilterEmailValidation.php
namespace Illuminate\Validation\Concerns;

use Egulias\EmailValidator\EmailLexer;
use Egulias\EmailValidator\Warning\Warning;
use Egulias\EmailValidator\Exception\InvalidEmail;
use Egulias\EmailValidator\Validation\EmailValidation;

class FilterEmailValidation implements EmailValidation
{
    /**
     * Returns true if the given email is valid.
     *
     * @param  string  $email
     * @param  EmailLexer
     * @return bool
     */
    public function isValid($email, EmailLexer $emailLexer)
    {
        return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
    }

    /**
     * Returns the validation error.
     *
     * @return InvalidEmail|null
     */
    public function getError()
    {
        //
    }

    /**
     * Returns the validation warnings.
     *
     * @return Warning[]
     */
    public function getWarnings()
    {
        return [];
    }
}

参考サイト

・emailのバリデーションについて
PHPで各種バリデーション
Gmailが日本語など非アルファベット文字を含むメールアドレスとの送受信に対応
RFC 5322 & 5321に沿ったメールアドレス(local-part)に使える文字まとめ
メールアドレスを表す現実的な正規表現
バリデーションで使用する正規表現の読み方

・ドメインについて
ドメイン名のしくみ

・Laravelバリデーションについて
Laravel 5.8 バリデーション
【Laravel 5.8.33】メールアドレスの新バリデーション・実例全5件!
バリデーションエラー/POST送信時のLaravelの挙動を追う

・Punycodeについて
Punycode
日本語JPドメイン名のPunycode変換・逆変換

43
32
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
43
32