LoginSignup
240
164

More than 3 years have passed since last update.

PHPでパスワード比較に==とか===使っちゃだめって知ってた?

Last updated at Posted at 2020-08-26

PHPerの皆さん、重要な文字列を入力データと保管情報を比較するときに比較演算子("=="とか"===")使ってませんか?
これ実は使っちゃだめなので、気をつけましょう。

なぜだめなの?

(2020/8/31修正) 関数名をtypoしていたため修正しました。(正)memcmp・(誤)strcmpです。

PHPを含む多くの言語では、文字列を比較する際に内部で memcmp() を使います。
通常の(厳密なセキュリティを必要としない)ケースでは、比較演算子を使うことはまったく問題ありません。
パスワードなど絶対に推測されてはいけない文字列を比較する場合、この関数は脆弱といえます。
memcmp() は内部で1バイトずつ比較検証するため、応答時間をもとに先頭から何文字正解だったか推測できます。
このような攻撃を「タイミング攻撃」といいます。

どうすればいいの?

PHPでは、ハッシュ値を用いて文字列を比較する hash_equals() という関数が用意されています。
関数の説明にもきちんと「Timing attack safe string comparison」と書かれています。

パスワードを扱う際の正しい実装

(2020/8/28追記) @anirfa さんのコメントをもとに追記しました。

ここまでは平文で保管された「重要な文字列」を扱う際の注意点でした。
たとえばパスワードなどの第三者から読み取らせてはいけない情報はそもそも平文で保管せず、ハッシュ化して保管すべきです。
PHPにはパスワード管理のために以下のような関数がありますので、そちらを使ってください。

実装をのぞいてみた

本当に比較演算子で strcmp() が使われてるかPHPソースコードをみてみましょう。

zend_string.h#L316-L335

zend_string.h
#if defined(__GNUC__) && (defined(__i386__) || (defined(__x86_64__) && !defined(__ILP32__)))
BEGIN_EXTERN_C()
ZEND_API zend_bool ZEND_FASTCALL zend_string_equal_val(zend_string *s1, zend_string *s2);
END_EXTERN_C()
#else
static zend_always_inline zend_bool zend_string_equal_val(zend_string *s1, zend_string *s2)
{
    return !memcmp(ZSTR_VAL(s1), ZSTR_VAL(s2), ZSTR_LEN(s1));
}
#endif

static zend_always_inline zend_bool zend_string_equal_content(zend_string *s1, zend_string *s2)
{
    return ZSTR_LEN(s1) == ZSTR_LEN(s2) && zend_string_equal_val(s1, s2);
}

static zend_always_inline zend_bool zend_string_equals(zend_string *s1, zend_string *s2)
{
    return s1 == s2 || zend_string_equal_content(s1, s2);
}

zend_string_equals->zend_string_equal_content->zend_string_equal_val->memcmpが呼び出されていますね。
※インラインアセンブラが使える場合の処理(zend_string.c#L396-431)の説明は割愛します

240
164
7

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
240
164