Pythonで入力検証するときに「^」と「$」は使っちゃいけない? それってどういうコト?HPかわさきの研究ノート

Pythonの正規表現で「$」って文字列末尾にマッチすると思っていませんか? 実はそうでもない……ってところから始まるいろいろを調べてみました。

» 2026年02月05日 05時00分 公開
[かわさきしんじDeep Insider編集部]

この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。

「HPかわさきの研究ノート」のインデックス

連載目次


かわさき

 どうもHPかわさきです。

 先日、編集長の一色の方から「これ、知ってる?」と「Use “\A...\z”, not “^...$” with Python regular expressions」のURLが送られてきました。簡単にいえば、正規表現で入力を検証するときに「^」と「$」というペアを使うよりも、「\A」と「\z」を使う方がいいよ、というお話です。

 そういうわけで、ちょっと調べてみました。


ざっくりとまとめると?

 ちょっと調べた結果を以下にまとめましょう。

  • Python 3.14以降で正規表現を使って入力の検証を行うときには「^」と「$」ではなく、「\A」と「\z」を使う方がよい(Python 3.14で「\z」がサポートされた)
  • というのは、Pythonでは「$」は「文字列末尾」もしくは「文字列末尾にある改行の直前」にマッチするから(re.MULTILINEだと各行末尾の改行の直前にもマッチ)
  • このことが入力の安全な検証を阻害する可能性がある
  • Pythonでは「\Z」は「文字列末尾にマッチする」が、言語(正規表現エンジン)によっては振る舞いが異なることがある
  • Python 3.14では「\z」が「\Z」と同様な意味を持つ特殊シーケンスとして使えるようになった(ただし、全ての言語で「\A」や「\z」を使えるわけではない。例:JavaScript)

 特に「$」が「文字列末尾」か「文字列末尾にある改行の直前」にマッチすることと、「\Z」の振る舞いが言語(正規表現エンジン)で異なる点には注意が必要です。というわけで、少し詳しく見ていきましょう。

「$」の振る舞いを確認する

 まずはPythonの正規表現における「$」の振る舞いを確認します。Pythonのreモジュールにある「Regular Expression Syntax」には「$」の振る舞いとして「Matches the end of the string or just before the newline at the end of the string, and in MULTILINE mode also matches before a newline」とあります。適当訳ですが、これは「$は文字列末尾もしくは文字列末尾にある改行の直前にマッチする。MULTILINEモードでは、改行の直前にもマッチする」となります(上でまとめた通りです)。

 ちょっとコードで確認してみます。

import re

s = 'foo\n'
p = r'foo$'
m = re.match(p, s)
repr(m.group())  # "'foo'":repr(m[0])でも可

「$」が文字列末尾ではなく、文字列末尾にある改行の直前でマッチしている

 このコードでは、re.match関数を使って、文字列がr'foo$'というパターンにマッチするかどうかを判定しています。re.match関数は文字列の先頭から比較を行うので、「^」は使っていません。「$」は文字列末尾か文字列末尾にある改行の直前にマッチするので、実行結果に示したように'foo'という文字列にマッチしました。

 と書いてしまうとそれで問題なさそうです(改行がどこにいったかはさておき)。そこで、今度はre.sub関数を使って「$」の振る舞いを確認してみましょう。

result = re.sub(r'$', '*', 'foo\n')
repr(result)  # "'foo*\\n*'"

「$」が2カ所にマッチしたので、re.sub関数で2カ所に「*」が挿入されている

 re.sub関数には単独の「$」をパターンとして、置換候補として「*」を渡しています(置換対象の文字列は先ほどと同じ'foo\n'です)。上で述べたように、「$」は文字列末尾に改行がある場合、文字列末尾(改行の直後)もしくは改行の直前にマッチするので、re.sub関数によりこの2カ所に「*」が挿入されます。

 これでまずは「$」が「文字列末尾」もしくは「文字列末尾にある改行の直前」にマッチすることは分かりました。

ログファイルを作る関数でユーザー名を検証してみる

 でも、特に最初のコード例を見ても、これがどんな問題を引き起こすかはよく分かりません。そこで次のようなコードを考えてみます。ユーザーとアクションを1行の文字列としてログにすることを想定しています(あくまでもサンプルで現実性に乏しいことはご承知ください)。

import re

def make_log(user, action):
    if not re.match(r'\w+$', user, flags=re.ASCII):
        raise ValueError('invalid username')

    return f'{user}, {action}\n'

user = 'deep_insider'
log = make_log(user, 'login')

#  何かしてログアウト
log += make_log(user, 'edit')

log += make_log(user, 'logout')
print(log)

make_log関数ではr'\w+$'という正規表現でユーザー名をチェックしている

 make_log関数ではr'\w+$'という正規表現でユーザー名をチェックしています。re.match関数呼び出し時にはフラグとしてre.ASCIIを指定しているので、これはユーザー名が大文字小文字のアルファベットと数字、アンダースコアだけで構成されているかどうかを判定していることになります。

 関数定義の後では、ユーザー名として'deep_insider'を指定して、make_log関数を何度か呼び出しています。そして、最後にログを表示していますが、この結果は次の通りです。

ユーザー名が'deep_insider'なら結果に問題なし ユーザー名が'deep_insider'なら結果に問題なし

 問題はなさそうですね。では、今後は文字列末尾に改行を含んだユーザー名'deep_insider\n'にしてみます。

user = 'deep_insider\n'
log = make_log(user, 'login')

# 何もせずログアウト

log += make_log(user, 'logout')
print(log)

ユーザー名の最後に改行が含まれている

 これを実行すると次のようになりました。

ログの構造が壊れた ログの構造が壊れた

 ユーザー名の検証を適当な正規表現で行っていたため、1行に1つのログのつもりが、そうはならなくなってしまいました。

 これはシンプルで、現実性にも乏しい例ですが、データ内の改行の有無で処理が切り替わるようなシステムがあった場合、不意な改行がよくないことを引き起こすことが考えられます(HTTPヘッダとHTTPボディは改行で区切られていますよね)。無理やりに改行を入れ込む攻撃手法のことを「CRLFインジェクション」などと呼びます(残念ながら、筆者はそこまで詳しくはないので、これ以上の言及はできないのですが)。

 というわけで、Pythonでは文字列末尾を「$」でマッチさせるのがあまりよろしくないことは分かりました。

「\A」と「\Z」でいいようで、よくない

 では「^」と「$」に変わるアンカーとして何が使えるかというと、Python 3.13までは「\A」と「\Z」が使えました(Python 3.14でももちろん使えます)。

  • \A:文字列先頭にマッチ
  • \Z:文字列末尾にマッチ

 実際に試してみましょう。ここではre.match関数を使うのでやっぱり「\A」は省略しましょう。

import re

s = 'foo\n'
p = r'foo\Z'
result = re.match(p, s)
print(result)  # None

文字列'foo\n'はパターンr'foo\Z'にマッチしない

 「\Z」は文字列末尾を意味するので、'foo'と文字列末尾の間に'\n'を含む文字列'foo\n'はパターンr'foo\Z'にはマッチしません。

 じゃあ、文字列末尾は「\Z」を使えばいいじゃない? と思うかもしれません。それで話が済めば問題はないのですが、実は言語によっては「\Z」が「文字列末尾にマッチ」するわけではないのです……。というか、JavaScriptでは「\A」も「\Z」も使えません。試してみましょう。

const p = /\Afoo\Z/;

const s0 = 'foo\n';
let result = p.test(s0);
console.log(result);  // false

/\Afoo\Z/と'foo\n'はマッチしない

 パターン/\Afoo\Z/は文字列の先頭にfooにそこで末尾となることを意味しています(なんで「\A」を使っているかは後で)。そして、文字列'foo\n'がこのパターンにマッチするかを試していますが、その結果はfalseです。

文字列'foo\n'はパターン/\Afoo\Zにマッチしない 文字列'foo\n'はパターン/\Afoo\Zにマッチしない

 ここで、以下のコードを試してみましょう。

const s1 = 'AfooZ';
result = p.test(s1);
console.log(result);  // true

文字列'AfooZ'にはマッチする

 文字列が'foo\n'ではなく、'AfooZ'に変わっていることに注目してください。実行結果は次の通りです。

文字列'AfooZ'はパターン/\Afoo\Z/にマッチする 文字列'AfooZ'はパターン/\Afoo\Z/にマッチする

 なんと、ヘンテコな文字列'AfooZ'が先ほどのパターンにマッチしました。つまり、「\A」は文字「A」をバックスラッシュでエスケープしている、つまり単なる「A」を意味しているということです。「\Z」も同様です。そのため、文字列'AfooZ'がこのパターンにマッチしたということです。

 実際にはパターンを/\Afoo\Z/uのようにuフラグ付きにすることで、このように存在しないエスケープシーケンスを記述するとSyntaxErrorが発生します。

 また、後出しですが、JavaScriptでは「$」は文字列末尾にマッチして、末尾が改行の場合にその直前にマッチするようなことはなさそうです。そういう意味では「\Z」がなくても問題はないといえます(ただし、「\Z」がないことで、他の言語と同じ正規表現を使えないという問題はありますが)。

 JavaScriptには「\Z」がないことは分かりました。この他にも「\Z」でも「文字列末尾に改行があれば、その直前にもマッチする」言語もあります。コードを出すのが面倒になってきたので、「regular expressions 101」を見てもらいましょう(このサイト、便利なので覚えておきましょう)。

Java 8では文字列"cat\n"はパターン"cat\Z"にマッチする Java 8では文字列"cat\n"はパターン"cat\Z"にマッチする

 詳しい説明は省略しますが、左側のペーンでは「Java 8」が選択されています。中央のペーンでは正規表現として「cat\Z」を、マッチ対象の文字列として「cat\n」を入力しています(「\n」ではなく改行マークになっていますね)。Java 8では「\Z」が文字列末尾の改行の直前にマッチするので、その結果、ここでは文字列の「cat」の背景色がうすい水色になり、右側のペーンでは「MATCH INFORMATION」にこのマッチについての情報が表示されています。

 このように、「\Z」の振る舞いは言語によってさまざまです。そのため、文字列末尾にマッチする正規表現に「\Z」を含めても、それを他の言語に持っていったときに使えるかどうかは分かりません。

 そこでPython 3.14でサポートが追加された「\z」の出番です。以下の画像はmake_log関数で「$」ではなく「\z」を使うようにして、改行を含むユーザー名を渡してみたところです。

不正なユーザー名が入力されたので例外を発生した 不正なユーザー名が入力されたので例外を発生した

 不正なユーザー名なので例外が発生しました。これはそのまま処理を続けてログを壊すよりも安全な挙動といえます。

 「\z」は多くの環境で「文字列末尾にマッチする」振る舞いをするようです(ただし、JavaScriptにはありません)。「What’s new in Python 3.14」にも「Support \z as a synonym for \Z in regular expressions. It is interpreted unambiguously in many other regular expression engines, unlike \Z, which has subtly different behavior.」とあります。日本語にテキトー訳すると「\Zの同義語として\zをサポート。他の多くの正規表現エンジンで\zは一意に解釈されている(それとは異なり、\Zは微妙に異なる振る舞いをする)」となるでしょう。

 まとめると次のようになります。

  • Pythonでは「$」は文字列末尾が改行の場合に振る舞いがめんどくさくて、セキュリティ的にも問題がある
  • 「\Z」は言語(正規表現エンジン)によって振る舞いが異なる
  • 「\z」は多くの環境で一意に(「文字列末尾にマッチする」と)解釈されている

 こうした理由から、Pythonで文字列の先頭から末尾を対象に入力を検証するときには、Python 3.13までの「\A」と「\Z」ではなく、「\A」と「\z」を使うのがよさそうということになります。他にもre.fullmatch関数を使うなどのやり方はあるはずです。でも、正規表現を使うのであれば、「$」は使わないようにするのが正解といえそうです。


かわさき

 ちょっと調べ始めたときには、そんなに書くことないんじゃない? と思ったら大間違いでした。しかも、セキュリティ面での考慮が必要だし、上で出したサンプルコードでは不十分じゃないかという気もするのですが、許してください。そうだ。MULTILINEモードについて言及できていないのがアレですが、そこも許してください(笑)。

 こんな難しいネタを振ってきた一色さんには感謝です(なぜかここでは「さん」付けだ。ま、私信みたいなものなので)。


「HPかわさきの研究ノート」のインデックス

HPかわさきの研究ノート

Copyright© Digital Advantage Corp. All Rights Reserved.

アイティメディアからのお知らせ

スポンサーからのお知らせPR

注目のテーマ

Microsoft & Windows最前線2026
人に頼れない今こそ、本音で語るセキュリティ「モダナイズ」
4AI by @IT - AIを作り、動かし、守り、生かす
AI for エンジニアリング
ローコード/ノーコード セントラル by @IT - ITエンジニアがビジネスの中心で活躍する組織へ
Cloud Native Central by @IT - スケーラブルな能力を組織に
システム開発ノウハウ 【発注ナビ】PR
あなたにおすすめの記事PR

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。