トップ «前の日記(2006-11-17) 最新 次の日記(2006-11-21)» 編集

2002|01|02|03|04|05|06|07|08|11|12|
2003|01|02|03|04|05|06|07|08|09|10|11|12|
2004|01|02|03|04|05|06|07|08|09|10|11|12|
2005|01|02|03|04|05|06|07|08|09|10|11|12|
2006|01|02|03|04|05|06|07|08|09|10|11|12|
2007|01|02|03|04|05|06|07|08|09|10|11|12|
2008|02|03|04|07|

2006-11-20 [長年日記]

_ PHPで安全なセッション管理を実現する方法に対する高木さんのコメントへのフォロー

高木さんのはてなブックマークコメントに、

[セキュリティ][乱数][暗号][PHP][moderate] PHPはセッションID生成にsecureな擬似乱数生成系を使用していないようだ。さすがPHPらしい駄目っぷり。

とあって、そこから人がたくさん来ているらしいんで、ちょっとだけフォロー。PHPのセッションID生成は、

sprintf(buf, "%.15s%ld%ld%0.8f", remote_addr ? remote_addr : "", 
 tv.tv_sec, (long int)tv.tv_usec, php_combined_lcg(TSRMLS_C) * 10);

なんて感じで、マイクロ秒単位の現在時刻+ユーザーのリモートアドレス+combined-LCG(線形合同法による乱数2つを組み合わせているらしい。線形合同法自体は、疑似乱数生成方法としてはセキュアな方法ではないとされている)による乱数を使って生成されているんだけど、php.iniとかで、

session.entropy_file = /dev/urandom
session.entropy_length = 16

とか設定しておけば、そっちも組み合わせて使われます(/dev/urandomとかが使える環境ならば)。なんで、この設定を有効にしておけば、PHPのセッションID生成もセキュアな感じになるんじゃないでしょうか。

ただわたしはセッションID生成におけるセキュリティの問題として、「マイクロ秒単位の現在時刻+ユーザーのリモートアドレス+combined-LCGによる乱数」という方法にはどの程度の問題があり、それに「/dev/urandom」を組み合わせることでどのくらい安全性が高まるのか、とかよく分かってませんが。

PHP 5.2.0の該当コード(ext/session/session.c)を貼っておきますよ

entropy_lengthとentropy_fileがphp.iniなどからやってくるsession.entropy_lengthとsession.entropy_file設定。デフォルトはどちらも空。hash_funcはsession.hash_function設定(PHP 5以降で有効)から来ていてデフォルトはMD5(値としては0)になっている。

PHPAPI char *php_session_create_id(PS_CREATE_SID_ARGS)
{
	PHP_MD5_CTX md5_context;
	PHP_SHA1_CTX sha1_context;
	unsigned char digest[21];
	int digest_len;
	int j;
	char *buf;
	struct timeval tv;
	zval **array;
	zval **token;
	char *remote_addr = NULL;

	gettimeofday(&tv, NULL);
	
	if (zend_hash_find(&EG(symbol_table), "_SERVER",
				sizeof("_SERVER"), (void **) &array) == SUCCESS &&
			Z_TYPE_PP(array) == IS_ARRAY &&
			zend_hash_find(Z_ARRVAL_PP(array), "REMOTE_ADDR",
				sizeof("REMOTE_ADDR"), (void **) &token) == SUCCESS) {
		remote_addr = Z_STRVAL_PP(token);
	}

	buf = emalloc(100);

	/* maximum 15+19+19+10 bytes */	
	sprintf(buf, "%.15s%ld%ld%0.8f", remote_addr ? remote_addr : "", 
			tv.tv_sec, (long int)tv.tv_usec, php_combined_lcg(TSRMLS_C) * 10);

	switch (PS(hash_func)) {
	case PS_HASH_FUNC_MD5:
		PHP_MD5Init(&md5_context);
		PHP_MD5Update(&md5_context, (unsigned char *) buf, strlen(buf));
		digest_len = 16;
		break;
	case PS_HASH_FUNC_SHA1:
		PHP_SHA1Init(&sha1_context);
		PHP_SHA1Update(&sha1_context, (unsigned char *) buf, strlen(buf));
		digest_len = 20;
		break;
	default:
		php_error_docref(NULL TSRMLS_CC, E_ERROR, "Invalid session hash function");
		efree(buf);
		return NULL;
	}

	if (PS(entropy_length) > 0) {
		int fd;

		fd = VCWD_OPEN(PS(entropy_file), O_RDONLY);
		if (fd >= 0) {
			unsigned char rbuf[2048];
			int n;
			int to_read = PS(entropy_length);
			
			while (to_read > 0) {
				n = read(fd, rbuf, MIN(to_read, sizeof(rbuf)));
				if (n <= 0) break;
				
				switch (PS(hash_func)) {
				case PS_HASH_FUNC_MD5:
					PHP_MD5Update(&md5_context, rbuf, n);
					break;
				case PS_HASH_FUNC_SHA1:
					PHP_SHA1Update(&sha1_context, rbuf, n);
					break;
				}
				to_read -= n;
			}
			close(fd);
		}
	}

	switch (PS(hash_func)) {
	case PS_HASH_FUNC_MD5:
		PHP_MD5Final(digest, &md5_context);
		break;
	case PS_HASH_FUNC_SHA1:
		PHP_SHA1Final(digest, &sha1_context);
		break;
	}

	if (PS(hash_bits_per_character) < 4
			|| PS(hash_bits_per_character) > 6) {
		PS(hash_bits_per_character) = 4;

		php_error_docref(NULL TSRMLS_CC, E_WARNING, "The ini setting hash_bits_per_character is out of range (should be 4, 5, or 6) - using 4 for now");
	}
	j = (int) (bin_to_readable((char *)digest, digest_len, buf, PS(hash_bits_per_character)) - buf);
	
	if (newlen) 
		*newlen = j;
	return buf;
}