ホーム > php

phpのアーカイブ

メールアドレス(addr-spec)の正規表現

  • 投稿者: chiba
  • 2009/3/22 日曜日 0:55:34
  • perl | php

能書き

前エントリを書いてからいろいろと調べていて驚いたんだけど、日本語のwebsiteで、それなりにまともにRFC822(RFC2822,RFC5322)に準拠した(もしくはきちんと意図的に準拠していない部分を選択している)正規表現はPerlだろうがPHPだろうがRubyだろうが軽くぐぐった程度では見当たらない。PerlのモジュールのEmail::AddressEmail::Validも程度の差はあれ問題を抱えている。そこらへんの既存の出回ってる正規表現にどういった問題があるかなんてことは次回エントリにて。

というわけで、Perl、PHP、RubyでRFC5322準拠なメールアドレス(addr-spec)の正規表現を以下に示します。尚、addr-specの最終的な正規表現のみならずそれを作成するに至る部分も併記してあります。これは、最終的な正規表現だけでは難解すぎてとても理解できないからです。内容を理解せずにそのままコピペすることを否定はしませんが理解しようとしたときの助けとなるよう、コメントアウトでもよいのでコード中に併記しておくことをお勧めします。

方針

  • RFC5322準拠が基本
  • addr_spec_looseは..や.@を許容した正規表現(日本のモバイルキャリア用)
  • ただしobsoleteなsyntaxは無視
  • ただしdomain_literalは無視
  • ただしCFWSは無視
  • ただしFWSも無視
  • 上記のように無視してるのが多いのは用途をweb入力のチェックやテキストからの抜き出し用途を想定しているため
  • BNFのsymbolの変数移植はできるだけRFCに即しつつ-を_に
  • 文字種の記述はできるだけRFCの順番にあわせる
  • ASCIIをコードポイントで指定する場合は16進数で
  • perlではflagged utf8でも処理できるように
  • できるだけテストはしてありますが、完璧だとは思っていないのでミスを指摘してくださる方は大歓迎です。

無視多すぎて準拠じゃないじゃんという突っ込みがありそうですが、自分で宣言している場合はありという俺ルールで。

Perl

動作確認: 5.10.0

my $wsp           = '[\x20\x09]';
my $vchar         = '[\x21-\x7e]';
my $quoted_pair   = "\\\\(?:$vchar|$wsp)";
my $qtext         = '[\x21\x23-\x5b\x5d-\x7e]';
my $qcontent      = "(?:$qtext|$quoted_pair)";
my $quoted_string = "\"$qcontent*\"";
my $atext         = '[a-zA-Z0-9!#$%&\'*+\-\/\=?^_`{|}~]';
my $dot_atom_text = "$atext+(?:[.]$atext+)*";
my $dot_atom      = $dot_atom_text;
my $local_part    = "(?:$dot_atom|$quoted_string)";
my $domain        = $dot_atom;
my $addr_spec     = qr{${local_part}[@]$domain};

my $dot_atom_loose   = "$atext+(?:[.]|$atext)*";
my $local_part_loose = "(?:$dot_atom_loose|$quoted_string)";
my $addr_spec_loose  = qr{${local_part_loose}[@]$domain};


my $input_addr_spec = 'foo@example.com';

if ( $input_addr_spec =~ /\A$addr_spec\z/ ) {
    print "valid addr-spec\n";
}

use utf8;
my $input_text = 'ぼくの@メールアドレスはfoo@example.comです';
if ( $input_text =~ /($addr_spec)/ ) {
    print "My addr-spec is <$1>\n";
}

PHP

動作確認: 5.2.6

<?php
$wsp           = '[\x20\x09]';
$vchar         = '[\x21-\x7e]';
$quoted_pair   = "\\\\(?:$vchar|$wsp)";
$qtext         = '[\x21\x23-\x5b\x5d-\x7e]';
$qcontent      = "(?:$qtext|$quoted_pair)";
$quoted_string = "\"$qcontent*\"";
$atext         = '[a-zA-Z0-9!#$%&\'*+\-\/\=?^_`{|}~]';
$dot_atom_text = "$atext+(?:[.]$atext+)*";
$dot_atom      = $dot_atom_text;
$local_part    = "(?:$dot_atom|$quoted_string)";
$domain        = $dot_atom;
$addr_spec     = "${local_part}[@]$domain";

$dot_atom_loose   = "$atext+(?:[.]|$atext)*";
$local_part_loose = "(?:$dot_atom_loose|$quoted_string)";
$addr_spec_loose  = "${local_part_loose}[@]$domain";


$input_addr_spec = 'foo@example.com';

if ( preg_match("/\A$addr_spec\z/", $input_addr_spec) ) {
    print "valid addr-spec\n";
}

$input_text = 'ぼくの@メールアドレスはfoo@example.comです';
if ( preg_match("/($addr_spec)/", $input_text, $matches) ) {
    print "My addr-spec is <$matches[0]>\n";
}

?>

Ruby

動作確認: 1.8.7

wsp           = '[\x20\x09]'
vchar         = '[\x21-\x7e]'
quoted_pair   = "\\\\(?:#{vchar}|#{wsp})"
qtext         = '[\x21\x23-\x5b\x5d-\x7e]'
qcontent      = "(?:#{qtext}|#{quoted_pair})"
quoted_string = "\"#{qcontent}*\""
atext         = '[a-zA-Z0-9!#$%&\'*+\-\/\=?^_`{|}~]'
dot_atom_text = "#{atext}+(?:[.]#{atext}+)*"
dot_atom      = dot_atom_text
local_part    = "(?:#{dot_atom}|#{quoted_string})"
domain        = dot_atom
addr_spec     = "#{local_part}[@]#{domain}"

dot_atom_loose   = "#{atext}+(?:[.]|#{atext})*"
local_part_loose = "(?:#{dot_atom_loose}|#{quoted_string})"
addr_spec_loose  = "#{local_part_loose}[@]#{domain}"

input_addr_spec = 'foo@example.com'

if /\A#{addr_spec}\z/ =~ input_addr_spec then
    puts "valid addr-spec"
end

input_text = 'ぼくの@メールアドレスはfoo@example.comです'
if /(#{addr_spec})/ =~ input_text then
    puts "My addr-spec is <#{$1}>";
end

テストコードこみのコード

以下にテストコード付きのものを貼り付けます。検証したいかたはどうぞ。テストケースは全言語共通になってます。

Perl

ファイル

#!/usr/bin/perl
use strict;
use Test::More;

my $wsp           = '[\x20\x09]';
my $vchar         = '[\x21-\x7e]';
my $quoted_pair   = "\\\\(?:$vchar|$wsp)";
my $qtext         = '[\x21\x23-\x5b\x5d-\x7e]';
my $qcontent      = "(?:$qtext|$quoted_pair)";
my $quoted_string = "\"$qcontent*\"";
my $atext         = '[a-zA-Z0-9!#$%&\'*+\-\/\=?^_`{|}~]';
my $dot_atom_text = "$atext+(?:[.]$atext+)*";
my $dot_atom      = $dot_atom_text;
my $local_part    = "(?:$dot_atom|$quoted_string)";
my $domain        = $dot_atom;
my $addr_spec     = qr{${local_part}[@]$domain};
print 'addr_spec: ' . $addr_spec, "\n";

my $dot_atom_loose   = "$atext+(?:[.]|$atext)*";
my $local_part_loose = "(?:$dot_atom_loose|$quoted_string)";
my $addr_spec_loose  = qr{${local_part_loose}[@]$domain};
print 'addr_spec_loose' . $addr_spec_loose, "\n";


my @valid = (
    'foo@example.com', # normal

    # local-part
     # dot-atom
     'foo.hoge@example.com',
     'foo.bar.baz@example.com',
     # quoted-string
     '"foo"@example.com',
     '"!"@example.com', # \x21
     '"#"@example.com', # \x23
     '"["@example.com', # \x5b
     '"]"@example.com', # \x5d
     '"["@example.com', # \x7e
      # quoted-pair
      '"\\ "@example.com', # \x20
      "\"\\\x09\"\@example.com", # \x09 # php @
      '"\\!"@example.com', # \x21
      '"\\["@example.com', # \x7e

    # domain
    'foo.hoge@localhost',
    'foo.hoge@sub.example.com',
);
my @valid_loose = (
    'foo.@docomo.ne.jp',
    'foo.foo.@docomo.ne.jp',
    'foo..@docomo.ne.jp',
    'foo..foo@docomo.ne.jp',
    'foo..foo.@docomo.ne.jp',
);

my @invalid = (
    '',
    'foo',
    'foo@',
    '@foo',
    # local-part
     # dot-atom
     '.foo@example.com',
     '..foo@example.com',
     'foo@@example.com',
     'foo[@example.com',
     'foo @example.com',
     # quoted-string
      "\"\x00\"\@example.com", # \x00 # php @
     '" "@example.com', # \x20
     '"""@example.com', # \x22
     '"\\"@example.com', # \x5c
      "\"\x7f\"\@example.com", # \x7f # php @
      # quoted-pair
      "\"\\\x1f\"\@example.com", # \x1f # php @
      "\"\\\x7f\"\@example.com", # \x7f # php @

    # \z check
    "foo\@example.com\n", # php @
    "foo\@example.com\nfoo\@example.com", # php @

    # non-ascii
    "\x80\@example.com",
    "\"\x80\"\@example.com",
    "\"\\\x80\"\@example.com",
    # utf8
    "\x100\@example.com",
    "\"\x100\"\@example.com",
    "\"\\\x100\"\@example.com",
);


plan tests => (@valid + @invalid + @valid_loose) * 2;

{
    # normal
    for (@valid) {
        ok( m{\A$addr_spec\z}o , 'normal-valid - ' . $_ );
    }

    for (@invalid, @valid_loose) {
        ok( !m{\A$addr_spec\z}o, 'normal-invalid - ' . $_ );
    }
}
{
    # loose
    for (@valid, @valid_loose) {
        ok( m{\A$addr_spec_loose\z}o , 'loose-valid - ' . $_ );
    }

    for (@invalid) {
        ok( !m{\A$addr_spec_loose\z}o, 'loose-invalid - ' . $_ );
    }
}
PHP

ファイル

<?php

$count = 0;

$wsp           = '[\x20\x09]';
$vchar         = '[\x21-\x7e]';
$quoted_pair   = "\\\\(?:$vchar|$wsp)";
$qtext         = '[\x21\x23-\x5b\x5d-\x7e]';
$qcontent      = "(?:$qtext|$quoted_pair)";
$quoted_string = "\"$qcontent*\"";
$atext         = '[a-zA-Z0-9!#$%&\'*+\-\/\=?^_`{|}~]';
$dot_atom_text = "$atext+(?:[.]$atext+)*";
$dot_atom      = $dot_atom_text;
$local_part    = "(?:$dot_atom|$quoted_string)";
$domain        = $dot_atom;
$addr_spec     = "${local_part}[@]$domain";
echo 'addr_spec: ' . $addr_spec, "\n";

$dot_atom_loose   = "$atext+(?:[.]|$atext)*";
$local_part_loose = "(?:$dot_atom_loose|$quoted_string)";
$addr_spec_loose  = "${local_part_loose}[@]$domain";
echo 'addr_spec_loose: ' . $addr_spec_loose, "\n";

$valid = array(
    'foo@example.com', # normal

    # local-part
     # dot-atom
     'foo.hoge@example.com',
     'foo.bar.baz@example.com',
     # quoted-string
     '"foo"@example.com',
     '"!"@example.com', # \x21
     '"#"@example.com', # \x23
     '"["@example.com', # \x5b
     '"]"@example.com', # \x5d
     '"["@example.com', # \x7e
      # quoted-pair
      '"foo\\ "@example.com', # \x20
      "\"foo\\\x09\"@example.com", # \x09 # php @
      '"\\!"@example.com', # \x21
      '"\\["@example.com', # \x7e

    # domain
    'foo.hoge@localhost',
    'foo.hoge@sub.example.com',
);
$valid_loose = array(
    'foo.@docomo.ne.jp',
    'foo.foo.@docomo.ne.jp',
    'foo..@docomo.ne.jp',
    'foo..foo@docomo.ne.jp',
    'foo..foo.@docomo.ne.jp',
);

$invalid = array(
    '',
    'foo',
    'foo@',
    '@foo',
    # local-part
     # dot-atom
     '.foo@example.com',
     '..foo@example.com',
     'foo@@example.com',
     'foo[@example.com',
     'foo @example.com',
     # quoted-string
      "\"\x00\"@example.com", # \x00 # php @
     '" "@example.com', # \x20
     '"""@example.com', # \x22
     '"\\"@example.com', # \x5c
      "\"\x7f\"@example.com", # \x7f # php @
      # quoted-pair
      "\"\\\x1f\"@example.com", # \x1f # php @
      "\"\\\x7f\"@example.com", # \x7f # php @

    # \z check
    "foo@example.com\n",
    "foo@example.com\nfoo@example.com",

    # non-ascii
    "\x80@example.com",
    "\"\x80\"@example.com",
    "\"\\\x80\"@example.com",
    # utf8
    "\x100@example.com",
    "\"\x100\"@example.com",
    "\"\\\x100\"@example.com",
);


{
    # normal
    foreach ($valid as $addr) {
        ok($addr_spec, $addr, 'normal-valid - ' . $addr);
    }

    foreach (array_merge($invalid, $valid_loose) as $addr) {
        not_ok($addr_spec, $addr, 'normal-invalid - ' . $addr);
    }
}
{
    # loose
    foreach (array_merge($valid, $valid_loose) as $addr) {
        ok($addr_spec_loose, $addr, 'loose-valid - ' . $addr);
    }

    foreach ($invalid as $addr) {
        not_ok($addr_spec_loose, $addr, 'loose-invalid - ' . $addr);
    }
}

function ok($regexp, $addr, $desc, $xor = 0) {
    global $count;
    $count++;
    if ( preg_match("/\A$regexp\z/", $addr) ^ $xor ) {
        echo "ok $count - $desc\n";
    }
    else {
        echo "not ok $count - $desc\n";
    }
}
function not_ok($regexp, $addr, $desc) {
    ok($regexp, $addr, $desc, 1);
}

?>

Ruby

ファイル

#!/usr/bin/ruby

wsp           = '[\x20\x09]'
vchar         = '[\x21-\x7e]'
quoted_pair   = "\\\\(?:#{vchar}|#{wsp})"
qtext         = '[\x21\x23-\x5b\x5d-\x7e]'
qcontent      = "(?:#{qtext}|#{quoted_pair})"
quoted_string = "\"#{qcontent}*\""
atext         = '[a-zA-Z0-9!#$%&\'*+\-\/\=?^_`{|}~]'
dot_atom_text = "#{atext}+(?:[.]#{atext}+)*"
dot_atom      = dot_atom_text
local_part    = "(?:#{dot_atom}|#{quoted_string})"
domain        = dot_atom
addr_spec     = "#{local_part}[@]#{domain}"
puts 'addr_spec: ' + addr_spec

dot_atom_loose   = "#{atext}+(?:[.]|#{atext})*"
local_part_loose = "(?:#{dot_atom_loose}|#{quoted_string})"
addr_spec_loose  = "#{local_part_loose}[@]#{domain}"
puts 'addr_spec_loose: ' + addr_spec_loose

valid = [
    'foo@example.com', # normal

    # local-part
     # dot-atom
     'foo.hoge@example.com',
     'foo.bar.baz@example.com',
     # quoted-string
     '"foo"@example.com',
     '"!"@example.com', # \x21
     '"#"@example.com', # \x23
     '"["@example.com', # \x5b
     '"]"@example.com', # \x5d
     '"["@example.com', # \x7e
      # quoted-pair
      '"foo\\ "@example.com', # \x20
      "\"foo\\\x09\"\@example.com", # \x09 # php @
      '"\\!"@example.com', # \x21
      '"\\["@example.com', # \x7e

    # domain
    'foo.hoge@localhost',
    'foo.hoge@sub.example.com',
]
valid_loose = [
    'foo.@docomo.ne.jp',
    'foo.foo.@docomo.ne.jp',
    'foo..@docomo.ne.jp',
    'foo..foo@docomo.ne.jp',
    'foo..foo.@docomo.ne.jp',
]

invalid = [
    '',
    'foo',
    'foo@',
    '@foo',
    # local-part
     # dot-atom
     '.foo@example.com',
     '..foo@example.com',
     'foo@@example.com',
     'foo[@example.com',
     'foo @example.com',
     # quoted-string
      "\"\x00\"\@example.com", # \x00 # php @
     '" "@example.com', # \x20
     '"""@example.com', # \x22
     '"\\"@example.com', # \x5c
      "\"\x7f\"\@example.com", # \x7f # php @
      # quoted-pair
      "\"\\\x1f\"\@example.com", # \x1f # php @
      "\"\\\x7f\"\@example.com", # \x7f # php @

    # \z check
    "foo@example.com\n",
    "foo@example.com\nfoo@example.com",

    # non-ascii
    "\x80\@example.com",
    "\"\x80\"\@example.com",
    "\"\\\x80\"\@example.com",
    # utf8
    "\x100\@example.com",
    "\"\x100\"\@example.com",
    "\"\\\x100\"\@example.com",
]


$count = 0
def ok(regexp, addr, desc, xor = 0)
    $count = $count + 1
    if  (/\A#{regexp}\z/ =~ addr) ^ xor then
        puts "ok #{$count} - #{desc}";
    else
        puts "not ok #{$count} - #{desc}";
    end
end

def not_ok(regexp, addr, desc)
    ok(regexp, addr, desc, 1)
end

# normal
valid.each do |addr|
    ok(addr_spec, addr, 'normal-valid - ' + addr);
end

(invalid + valid_loose).each do |addr|
    not_ok(addr_spec, addr, 'normal-invalid - ' + addr);
end

# loose
(valid + valid_loose).each do |addr|
    ok(addr_spec_loose, addr, 'loose-valid - ' + addr);
end

invalid.each do |addr|
    not_ok(addr_spec_loose, addr, 'loose-invalid - ' + addr);
end

そろそろCakePHPについて一言言っておくか

  • 投稿者: chiba
  • 2008/12/7 日曜日 5:18:14
  • php

前回のエントリを見てくれた人がCakePHPのtracにこれ脆弱性じゃね?ってチケットを立ててくれたよ!で、見に行ってみたらCakePHPのエライ人がこんな回答をしてくれてたよ!

#5842(getClientIP() possiblly return false IP address)

this is not a security exploit in Cake,
but should certainly be something to be aware of when building an application.

超意訳: CakePHPの脆弱性ではありません。「仕様です」。だからアプリケーション構築するさいはちゃんと知っとくべきことだよね!

オーケー、分かった。確かにこれはCakePHPの脆弱性ではなかった。CakePHPを使っていた僕らの脆弱性だったんだ。オーケー、前言は撤回するよ。正しくはこうだったんだ。

CakePHPが許されるのは小学生までだよねー

CakePHPのgetClientIPを使っていいのは小学生までだよねー

  • 投稿者: chiba
  • 2008/12/3 水曜日 5:44:14
  • php

PHPで開発をすることが多くなりPerlの良さを再確認している今日この頃ですが皆さんいかがお過ごしでしょうか。

さて、今日は今もっともナウいPHPのWebアプリケーションフレームワークであるCakePHPのお話を一つ。

CakePHPには組み込みコンポーネントとしてリクエストハンドラ(RequestHandler)が備わっています。
リクエストハンドリング :: 組み込みのコンポーネント :: マニュアル :: 1.2 Collection :: The Cookbook:

このRequestHandlerのメソッドであるgetClientIPが小学生には危険そうだというお話。(あ、このエントリのタイトル逆w)

まずはgetClientIPの実装コードを。(1.2を例にとっているが1.1もほぼ一緒である)
https://trac.cakephp.org/browser/trunk/cake/1.2.x.x/cake/libs/controller/components/request_handler.php

function getClientIP() {
    if (env('HTTP_X_FORWARDED_FOR') != null) {
        $ipaddr = preg_replace('/(?:,.*)/', '', env('HTTP_X_FORWARDED_FOR'));
    } else {
        if (env('HTTP_CLIENT_IP') != null) {
            $ipaddr = env('HTTP_CLIENT_IP');
        } else {
            $ipaddr = env('REMOTE_ADDR');
        }
    }

    if (env('HTTP_CLIENTADDRESS') != null) {
        $tmpipaddr = env('HTTP_CLIENTADDRESS');

        if (!empty($tmpipaddr)) {
            $ipaddr = preg_replace('/(?:,.*)/', '', $tmpipaddr);
        }
    }
    return trim($ipaddr);
}

(envの実装はbasics.php。簡単にいってしまえば$_SERVERか$_ENVから指定されたkeyの値をとってくるというところ。)
ひええ。怖いですね。小学生には使いこなすのは厳しそうですね。それではみなさんご機嫌よう。

といって終わるのも不親切なので何がどう危険なのかを少し解説。

HTTP_X_FORWARDED_FORというのはhttpヘッダのFORWARDED_FORX-FORWARDED-FORのことで、これは一般的には、匿名ではないプロキシに接続している際にプロキシサーバがアクセス先のサーバにhttpリクエストのヘッダに勝手に送信元のIPアドレスを付与しちゃうものだ。

で、このコードではそのヘッダに何か値があればそれを送信元IPアドレスとして返してしまう。

ちなみにリバースプロキシを使っている時はmod_rpafmod_extract_forwardedを使ってこのコードと同様のことをapacheレベルでやったりはする(mod_rpafとmod_extract_forwardedは置き換えのフックのタイミングが違ってたりする)。しかし決定的に違うのは、mod_rpafにしろmod_extract_forwardedにしろ、置き換えを許す送信元のプロキシのアドレスを限定する設定があるのだ。mod_rpafではRPAFproxy_ips、mod_extract_forwardedではMEFacceptがそれにあたる。ところがこのgetClientIPにはそういったコードは見当たらない。

ここで一度整理しておきたいが、phpで取得できる$_SERVER[‘REMOTE_ADDR’]はwebサーバに実際に接続してきたtcp/ipにおける送信元アドレスであり、3ウェイ・ハンドシェイクにより検証されている信頼性の高い送信元IPアドレスといえる(もちろんシーケンス番号の問題とかもあるが)。それに比べhttpリクエストヘッダに記述されるFORWARDED_FORX-FORWARDED-FORの値は、webサーバにとってはまったく検証を経ていない信頼性の低いアドレスなのである。

上記のことからわかるのはgetClientIPには任意のIPアドレスを注入できるということだ。要するにIP偽装(IP Spoofing)である。

ちなみにfirefoxであればアドオンのModify Headersなんかを使えば簡単にhttpヘッダは任意のものが作成できる。

ところで、WebアプリケーションにおいてIP偽装が問題になるのはどういう場面であろうか。それは認証に利用している場合であろう。日本に住んでいるwebエンジニアが真っ先に思いつくのは携帯の個体識別番号認証でのIPアドレスチェックではないだろうか。先日高木浩光氏に退化してゆく日本のWeb開発者と揶揄されていたが、個体識別番号認証自体を完全否定することは私にはできないが、少なくともCakePHPでモバイルサイトを構築されている方はこの件を確認していただくのがよいかとおもわれる。しかしたとえば携帯とPCでデザインを変えているだけなんて場合には影響は皆無であろう。

ちなみにこの件はCakePHP開発側に伝えるべきなのだろうか。しかしこれだけ確信的にコードに書かれていると「仕様です」という感じがしないでもない。

しかし、小学生が使うことも考慮して以下のマニュアル等には「よいこのみなさん、ここでえることのできるIPアドレスはほんとうのクライアントのIPアドレスとはちがうばあいがあるよ」ぐらいは書いておいてほしいものだ。
クライアントについての追加情報を取得する :: リクエストハンドリング :: 組み込みのコンポーネント :: マニュアル :

まぁとりあえず小学生は$_SERVER[‘REMOTE_ADDR’]を普通に使っておくとよいとおもわれる。

追記1: $_SERVER[‘HTTP_X_FORWARDE_FOR’]のhttpヘッダでのキーは”X-FORWARDED-FOR”ですた

ホーム > php

検索
フィード
メタ情報

ページの上部に戻る