はじめに
この記事では以下のような内容を説明します。
- Railsの正規表現(入力値バリデーション)でよく使われる
\A
や\z
とは何なのか -
\A
や\z
は^
や$
とどう違うのか - JavaScript のような他の言語でも同じように
\A
や\z
を使えるのか
「なんかよくわからないけど、^
や $
を使うとRailsに怒られるから \A
や \z
を使ってる」という人は、ぜひこの記事を読んできちんと意味を理解しましょう!
前提となる知識
正規表現についての基本的な説明はここではしません。
正規表現が全く分からない、という方は以下の記事を読んでおいてください。
初心者歓迎!手と目で覚える正規表現入門・その1「さまざまな形式の電話番号を検索しよう」 - Qiita
初心者歓迎!手と目で覚える正規表現入門・その2「微妙な違いを許容しつつ置換しよう」 - Qiita
初心者歓迎!手と目で覚える正規表現入門・その3「空白文字を自由自在に操ろう」 - Qiita
それでは以下が本編です。
例題:Railsで電話番号のフォーマットチェックを行う
Railsでは入力値のフォーマットチェックに正規表現が使えます。
たとえば電話番号の(簡易的な)フォーマットチェックをしたい場合は次のように書きます。
validates :phone_number, format: { with: /\A\d+-\d+-\d+\z/ }
また、Rails 3の時代は以下のように書くこともできました。
validates :phone_number, format: { with: /^\d+-\d+-\d+$/ }
しかし、Rails 4 以降で ^
や $
を使うと次のようなエラーが発生します。
The provided regular expression is using multiline anchors (^ or $), which may present a security risk. Did you mean to use \A and \z, or forgot to add the :multiline => true option? (ArgumentError)
翻訳するとこんなことが書いてあります。
指定された正規表現は複数行アンカー(
^
または$
)が使われています。これはセキュリティリスクになりえます。もしかすると\A
と\z
の書き間違い、もしくは:multiline => true
オプションの付け忘れではありませんか?
そうなんです。
^
と $
を使うとセキュリティリスクになるんです。
その理由をじっくり説明していきます。
Step 1. でたらめな入力をNGとする
まず、電話番号であれば "I have a pen" のような入力値はNGとしなければなりません。
これは /\d+-\d+-\d+/
という正規表現を使えばフィルタリングできます。
"I have a pen" =~ /\d+-\d+-\d+/
# => nil (Rubyの世界では偽)
参考までに、Rubularでの実行結果も一緒に載せておきます。(以下同様)
ご覧のとおり、"I have a pen" は /\d+-\d+-\d+/
にはマッチしません。
Step 2. 余計な文字列が入っていたらNGとする
では次に、もう少し入力値のバリエーションを考えてみましょう。
"03-1234-5678" は当然OKですが、"Call me, 03-1234-5678" はNGです。
しかし、/\d+-\d+-\d+/
だけだと、OKになってしまいます。
"Call me, 03-1234-5678" =~ /\d+-\d+-\d+/
# => 9 (Rubyの世界では真)
なぜパスしてしまうのかというと、/\d+-\d+-\d+/
だと「電話番号以外、余計な文字列が含まれない」という条件が含まれていないからです。
これを防止するために、(Railsの世界以外では) ^
と $
がよく使われます。
つまり、/^\d+-\d+-\d+$/
と書くことで、「最初から最後まで \d+-\d+-\d+
に合致する文字列じゃないとダメだよ。余計な文字が前後に入っちゃダメだよ」と条件を厳しくするわけです。
"Call me, 03-1234-5678" =~ /^\d+-\d+-\d+$/
# => nil (Rubyの世界では偽)
これで "Call me, 03-1234-5678" はNGとなりました。
【重要】Step 3. 複数行テキストが渡されてもNGとする
さて、Rails 3までは話はここで終わっていたのですが、Rails 4からは話が変わりました。
実はRubyの正規表現では ^
と $
はあくまで「行頭」と「行末」を意味するメタ文字であって、「文字列の先頭」と「文字列の末尾」を意味するメタ文字ではありません。
そのため、複数行テキストが渡されると意図しない入力値をパスさせてしまう恐れがあります。
・・・といっても、言葉だけではピンと来ないと思うので、サンプルコードを使いましょう。
たとえば、以下のような文字列は /^\d+-\d+-\d+$/
では(あなたの予想に反して)真と判断されます。
<script>alert('XSS!!');</script>
03-1234-5678
以下はRuby上での実行結果です。
dangerous_phone_number = <<-TEXT
<script>alert('XSS!!');</script>
03-1234-5678
TEXT
dangerous_phone_number =~ /^\d+-\d+-\d+$/
# => 33 (Rubyの世界では真)
これで想像が付いたでしょうか?
「/^\d+-\d+-\d+$/
は1行目にはマッチしなかったけど、2行目にはマッチした、だから入力値チェック的にはOK!!」と判断されてしまうのです。
しかし、当然ながらこんな入力値を許容してはいけません。
そこで、\A
と \z
が登場します。
これはそれぞれ「文字列の先頭」と「文字列の末尾」を意味します。
なので、/\A\d+-\d+-\d+\z/
と書けば、先ほどの怪しい電話番号はNGと判断されます。
dangerous_phone_number = <<-TEXT
<script>alert('XSS!!');</script>
03-1234-5678
TEXT
dangerous_phone_number =~ /\A\d+-\d+-\d+\z/
# => nil (Rubyの世界では偽)
このように、^
と $
では怪しい複数行の入力値をバリデーションエラーにすることができないため、Rails 4では基本的に \A
と \z
を使うことが必須になっています。
Rails 4であえて ^ と $ の使用を許可する場合
ただし、最初の方に挙げたRailsのエラーメッセージをよく読むと「もしくは :multiline => true
オプションの付け忘れではありませんか?」と書いてありました。
実際、次のようにバリデーションを定義すると、^
と $
を使ってもエラーにはなりません。
validates :phone_number, format: { with: /^\d+-\d+-\d+$/, multiline: true }
これはテキストエリアの入力値のように、 意図的に 複数行の入力値に対して正規表現を使いたい場合のオプションです。
たとえば、(現実的かどうかはさておき)次のように「必ず電話番号を書いてください」というようなお問い合わせ欄を作った場合は /^\d+-\d+-\d+$/
で入力値チェックができます。
上の図のような入力値であれば、/^\d+-\d+-\d+$/
の入力値チェックとしてはOKになります。
とはいえ、僕はこれまで multiline: true
オプションを使ったことがないので、登場頻度はかなり少ないんじゃないかと思います。
\z と \Z の違い
ところで、ここまで読んできた方は \A
と \z
を見て「なんで片方が大文字で、片方が小文字なんだ?」と思った人がいるかもしれません。(僕は最初そう思いました)
実は \z
だけでなく \Z
というメタ文字も存在します。
Rubyのリファレンスマニュアルには次のように説明されています。
-
\Z
文字列の末尾にマッチします。 ただし文字列の最後の文字が改行ならばそれの手前にマッチします。 -
\z
文字列の末尾にマッチします。
うーん、これまたピンと来ない説明ですね。
というわけで、サンプルコードを使って動きの違いを確認してみましょう。
# 末尾が改行文字、正規表現は小文字の \z
"03-1234-5678\n" =~ /\A\d+-\d+-\d+\z/
# => nil (Rubyの世界では偽)
# 末尾が改行文字、正規表現は大文字の \Z
"03-1234-5678\n" =~ /\A\d+-\d+-\d+\Z/
# => 0 (Rubyの世界では真)
# 改行文字の後にも文字列、正規表現は大文字の \Z
"03-1234-5678\n<script>alert('XSS!!');</script>" =~ /\A\d+-\d+-\d+\Z/
# => nil (Rubyの世界では偽)
# 末尾に改行文字なし、正規表現は大文字の \Z
"03-1234-5678" =~ /\A\d+-\d+-\d+\Z/
# => 0 (Rubyの世界では真)
これで理解できたでしょうか?
簡単にいうと「文字列の末尾が改行文字で終わっても許可するのが大文字の \Z
」ということです。
とはいえ、僕はこれまで \Z
を使ったことがありません。
Railsでは \Z
を使ってもエラーにはなりませんが、実際の開発では \A
と \z
の組み合わせで使うことの方が圧倒的に多いと思います。
JavaScriptには \A や \z がありません
さて、ここまで「Rubyは~」「Railsは~」という説明をしてきましたが、他の言語ではどうなんでしょうか?
JavaScriptで /\A\d+-\d+-\d+\z/
を使った入力値チェックを行うとこうなります。
var phoneNumber = "03-1234-5678";
var regex = /\A\d+-\d+-\d+\z/;
console.log(phoneNumber.match(regex));
// => null
あれ?マッチしませんね。。
実はJavaScriptには \A
や \z
というメタ文字がありません。
特にエラーなく正常に動いているように見えますが、\A
や \z
は意味なくエスケープされている "A" や "z" そのものです。
実際、"A03-1234-5678z" という文字列を与えると /\A\d+-\d+-\d+\z/
にマッチします。
var phoneNumber = "A03-1234-5678z";
var regex = /\A\d+-\d+-\d+\z/;
console.log(phoneNumber.match(regex));
// => [ 'A03-1234-5678z', index: 0, input: 'A03-1234-5678z' ]
JavaScriptの場合、^
と $
は「文字列の先頭」と「文字列の末尾」の意味になるので、^
と $
で入力値チェックをすればOKです。
var phoneNumber = "<script>alert('XSS!!');</script>\n03-1234-5678";
var regex = /^\d+-\d+-\d+$/;
console.log(phoneNumber.match(regex));
// => null
phoneNumber = "03-1234-5678";
console.log(phoneNumber.match(regex));
// => [ '03-1234-5678', index: 0, input: '03-1234-5678' ]
逆に、Rubyのように ^
と $
を「行頭」と「行末」の意味にする場合は、正規表現に m
オプション(複数行オプション)を付けます。
var phoneNumber = "<script>alert('XSS!!');</script>\n03-1234-5678";
var regex = /^\d+-\d+-\d+$/m;
console.log(phoneNumber.match(regex));
// => ["03-1234-5678", index: 33, input: "<script>alert('XSS!!');</script>↵03-1234-5678"]
ちなみにAtomを使った場合も同様に \A
や \z
はメタ文字になりません。
(Atomの正規表現エンジン ≒ JSの正規表現エンジンなので)
このように、言語や環境によって使えるメタ文字が異なったり、同じメタ文字でも微妙に働きが違ったりすることがよくあります。
ソースコードに正規表現を埋め込んだりする場合はしっかり動作確認するようにしましょう。
まとめ
本記事では以下のようなことを学びました。
- Rubyでは
^
や$
は「行頭」「行末」に意味になる。そのため、本来パスさせるべきでない複数行テキストをパスさせてしまうことがある。 -
\A
や\z
を使うと「文字列の先頭」「文字列の末尾」の意味になるため、意図しない複数行テキストの入力をNGとすることができる。 -
\Z
は\z
とよく似ているが、「文字列の末尾が改行文字で終わってもOK」という点が異なる。 -
\A
や\z
が使えるかどうかは言語によって異なる。たとえばJavaScriptでは\A
や\z
に特殊な意味はない(ただの文字として扱われる)。 - Rubyとは異なり、JavaScriptのデフォルトでは
^
と$
が「文字列の先頭」「文字列の末尾」の意味になる。このように言語や環境によってメタ文字の意味が微妙に異なる場合がある。
というわけで、この記事ではRailsの正規表現でよく使われる \A
や \z
の役割を詳しく説明してみました。
^
や $
が入っているとエラーが発生してRailsが起動しないので、^
や $
を使い続けている人はいないと思います。
ただし「よくわからないまま \A
や \z
を使っていた」という人はこの記事を読んで、その意味を理解してくれたら嬉しいです。
あわせて読みたい
「一通り読んでみたけど、そもそも正規表現をちゃんと理解できてないわー」と思った人は、「手と目で覚える正規表現入門」シリーズをぜひ読んでみてください。
初心者歓迎!手と目で覚える正規表現入門・その1「さまざまな形式の電話番号を検索しよう」 - Qiita
初心者歓迎!手と目で覚える正規表現入門・その2「微妙な違いを許容しつつ置換しよう」 - Qiita