PHPerの皆さん、重要な文字列を入力データと保管情報を比較するときに比較演算子("=="とか"===")使ってませんか?
これ実は使っちゃだめなので、気をつけましょう。
なぜだめなの?
(2020/8/31修正) 関数名をtypoしていたため修正しました。(正)memcmp・(誤)strcmpです。
PHPを含む多くの言語では、文字列を比較する際に内部で memcmp()
を使います。
通常の(厳密なセキュリティを必要としない)ケースでは、比較演算子を使うことはまったく問題ありません。
パスワードなど絶対に推測されてはいけない文字列を比較する場合、この関数は脆弱といえます。
memcmp()
は内部で1バイトずつ比較検証するため、応答時間をもとに先頭から何文字正解だったか推測できます。
このような攻撃を「タイミング攻撃」といいます。
どうすればいいの?
PHPでは、ハッシュ値を用いて文字列を比較する hash_equals()
という関数が用意されています。
関数の説明にもきちんと「Timing attack safe string comparison」と書かれています。
パスワードを扱う際の正しい実装
(2020/8/28追記) @anirfa さんのコメントをもとに追記しました。
ここまでは平文で保管された「重要な文字列」を扱う際の注意点でした。
たとえばパスワードなどの第三者から読み取らせてはいけない情報はそもそも平文で保管せず、ハッシュ化して保管すべきです。
PHPにはパスワード管理のために以下のような関数がありますので、そちらを使ってください。
- password_hash():パスワードを安全に保管する
- password_verify():保管されたパスワードを検証する
実装をのぞいてみた
本当に比較演算子で strcmp()
が使われてるかPHPソースコードをみてみましょう。
#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)の説明は割愛します