Laravelデフォルトのバリデーションルール「email」を使用したWEBサービスを作成したところ、バリデーション不足を指摘された。
環境
Laravel 5.8.38
#問題点
ユーザー登録画面にて、test@example.comあいうえお
といった、@以降にひらがなを含むメールアドレスを登録できてしまう。
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'],
]);
}
実際に登録されたメールアドレス
test@example.xn--com-m63bkmoq
まじか・・・
###結論
'email'
↓
'email:strict,dns,spoof'
とりあえずこれ。dnsによりドメインが存在するアドレスのみバリデーションを通るので、意味不明な日本語を入れたらまず通らないようにはできると思う。
が、本当にこれが正しいのか分からないので、色々調べてみる。
#何が起きているのか
Laravelの公式ドキュメントによれば、
フィールドがメールアドレスとして正しいことをバリデートします。内部でこのバリデーションルールはメールアドレスの検証に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
に変換された。
###カスタムバリデーションで正規表現を使ってみる
正規表現を使ったカスタムバリデーションを試そうともしたが、Punycodeでアルファベット、数字、ハイフンのみの文字列に変換されてしまうのならば、RFCValidation等と同じ結果になってしまうのではないだろうか?
念のため試してみる。
emailの正規表現は難しいらしく、どれを使えば良いのか・・・という感じだが、こちらの記事を参考にさせていただいた。
メールアドレスを表す現実的な正規表現
自作のバリデーションルール
public function passes($attribute, $value)
{
return preg_match("/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/", $value);
}
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
は、併用が可能。
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
等、他のバリデーションも併用すると良さそう。
ただ、バリデーションの種類云々と同じくらい大事なのが、「どの程度のバリデーションでサービスの依頼主は満足するか?」を、質問なり打ち合わせでしっかり確認しておくことだと感じた。(僕は何も確認せずに納品してしまったので)
記事の内容等に間違いがありましたら、申し訳ございません。
#おまけ
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));
}
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変換・逆変換