【PHP】php.ini Security Deep Dive

PHPerの皆さんはphp.iniをちゃんと設定していますか?
全てデフォルト値を使用しているなんてことありませんよね? (煽り

セキュリティの観点から設定した方がいいパラメータの設定値を可能な限り説明を交えつつ紹介していきたいと思います :muscle:

# 前提

![](https://img.shields.io/badge/PHP-7.3-blue.svg)

# php.ini

php.iniで設定可能な全パラメータは[公式ドキュメント](https://www.php.net/manual/ja/ini.list.php)、[PHP-7.3/php.ini-production](https://github.com/php/php-src/blob/PHP-7.3/php.ini-production)を参照ください
では、セキュリティの観点から設定した方がいいパラメータについて説明していきます

## open_basedir

### 説明

PHPによってアクセス可能なディレクトリを制限します
デフォルトでは全てのディレクトリにアクセス可能です
directory traversalなどの攻撃を防御するために必ず設定をしておきましょう

基本的には/var/www/htmlを指定するだけで十分なケースが多いですが、`:`を区切り文字として複数のディレクトリを指定可能です

### DeepDive
open_basedirの実装を追ってみましょう

![plantuml (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/173064/fde7e678-9d0b-afdd-48dd-5ac246b0b8cf.png)

以下はphp_check_open_basedir_exのコアな処理を抜粋したコードです
https://github.com/php/php-src/blob/PHP-7.3/main/fopen_wrappers.c#L300-L319

```c:php_check_open_basedir_ex
while (ptr && *ptr) {
	end = strchr(ptr, DEFAULT_DIR_SEPARATOR);
	if (end != NULL) {
		*end = '\0';
		end++;
	}

	if (php_check_specific_open_basedir(ptr, path) == 0) {
		efree(pathbuf);
		return 0;
	}

	ptr = end;
}
if (warn) {
	php_error_docref(NULL, E_WARNING, "open_basedir restriction in effect. File(%s) is not within the allowed path(s): (%s)", path, PG(open_basedir));
}
efree(pathbuf);
errno = EPERM; /* we deny permission to open it */
return -1;
```

DEFAULT_DIR_SEPARATORは`:`として定義されています

php_check_specific_open_basedirの返り値が0 ( = open_basedirで許可されている)か、strchr(ptr, DEFAULT_DIR_SEPARATOR)の返り値がNULL ( = open_basedirの設定値による探査終了) になるまでループします

`if (warn)`ではphp_check_open_basedirから呼び出される時に第2引数warnに１を指定しているので、open_basedirに指定されていないディレクトリを呼び出そうとすると`php_error_docref`によって警告が出力されます
警告用の引数を渡しておいて、その条件分岐前にreturnするかどうかで警告出すかどうかを決めるのはいい実装方法ですよね
内部で警告出すフラグをゴニョゴニョする必要がなく、すっきりしている印象を抱きます

---

php_check_specific_open_basedirはsymlinkにも対応しているので、ファイル本体がopen_basedir外であればエラーになります
以下がsymlinkの処理を行っているコードです
https://github.com/php/php-src/blob/PHP-7.3/main/fopen_wrappers.c#L167-L181

```c:php_check_specific_open_basedir
#if defined(PHP_WIN32) || defined(HAVE_SYMLINK)
		if (nesting_level == 0) {
			ssize_t ret;
			char buf[MAXPATHLEN];

			ret = php_sys_readlink(path_tmp, buf, MAXPATHLEN - 1);
			if (ret == -1) {
				/* not a broken symlink, move along.. */
			} else {
				/* put the real path into the path buffer */
				memcpy(path_tmp, buf, ret);
				path_tmp[ret] = '\0';
			}
		}
#endif
```

### 余談
ちなみにopen_basedirを設定するとrealpath cacheは使用できなくなるので注意が必要です
realpath cacheの設定をしていて、「あれ?」とならないようにしましょう
タイプは違えどopcacheは利用できます
open_basedirを設定している場合にrealpath cacheを無効にしているコードは以下になります
https://github.com/php/php-src/blob/PHP-7.3/main/main.c#L1802-L1805

```c
/* Disable realpath cache if an open_basedir is set */
if (PG(open_basedir) && *PG(open_basedir)) {
  CWDG(realpath_cache_size_limit) = 0;
}
```

realpath_cache_size_limitを0に設定して無効化しています
シンプルで美しい :sparkles: (ただ言いたかっただけw

## disable_functions

### 説明

実行禁止する関数を設定できます
一般的なアプリケーションはファイルシステム・OSユーザなどをいじることはないと思うので、これらの変更が可能な関数は禁止しておきましょう
disable_fuctionsを設定するTipsとして、最初は厳し目に設定しておいてテストなどを通じて実行が必要な関数を設定から削除していくのがいいでしょう
自身で書いたコード以外にも使用しているライブラリやフレームワークなどにも影響が出るので注意が必要です

(1行だと見にくいと思うのでリスト化していますが、php.iniに書く際は`,`を区切り文字として1行で書きます)

```:設定した方がいい関数例
- system
- exec
- shell_exec
- passthru
- phpinfo (開発時は許可してもよい)
- show_source,
- highlight_file
- popen
- fopen_with_path
- dbmopen
- dbase_open
- putenv
- move_uploaded_file
- chdir
- mkdir
- rmdir
- chmod
- rename
- filepro
- filepro_rowcount
- filepro_retrieve
- posix_* (prefixがposixである関数)
```

### DeepDive

disable_functionsの実装を追っていきましょう

![plantuml (3).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/173064/93e48730-54e0-566a-be43-011f8c5c9371.png)

disable_functionsはphp_module_startup、つまり起動時に読み込まれます
なので、PHPスクリプトで上書きすることはできません

PHPの関数はfunction_tableというハッシュテーブルで管理されています
disable_functionsで指定されて関数はfunction_tableから削除されるわけではなく、関数のハンドラーにdisplay_disabled_functionというエラーハンドラー(zend_error)をラップした関数をセットします
display_disabled_functionはエラーを発生させて、セキュリティの設定上関数が無効化されているメッセージを出力します
以下は関数を無効化するzend_disable_functionのコードです
https://github.com/php/php-src/blob/PHP-7.3/Zend/zend_API.c#L2847-L2859

```c:zend_disable_function
ZEND_API int zend_disable_function(char *function_name, size_t function_name_length) /* {{{ */
{
	zend_internal_function *func;
	if ((func = zend_hash_str_find_ptr(CG(function_table), function_name, function_name_length))) {
	  func->fn_flags &= ~(ZEND_ACC_VARIADIC | ZEND_ACC_HAS_TYPE_HINTS | ZEND_ACC_HAS_RETURN_TYPE);
		func->num_args = 0;
		func->arg_info = NULL;
		func->handler = ZEND_FN(display_disabled_function);
		return SUCCESS;
	}
	return FAILURE;
}
/* }}} */
```

zend_hash_str_find_ptrによってfunction_tableから無効化したい関数を探査します
そして、該当の関数が存在すれば無効化します

## expose_php

### 説明

expose_phpをOffにすると、PHPで処理していることやバージョンをHTTP Headerに含めないようにできます
プロダクションでは必ずOffにすべきパラメータです

### DeepDive

![plantuml (5).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/173064/150b7da7-e06a-af9a-0a67-8884e368a62e.png)

HTTP Headerにセットする内容は以下のように定義されています
https://github.com/php/php-src/blob/PHP-7.3/main/SAPI.h#L289

```c:SAPI.h
#define SAPI_PHP_VERSION_HEADER		"X-Powered-By: PHP/" PHP_VERSION
```

## display_errors

### 説明

実行時のPHPエラーをHTMLの一部として画面に出力するかどうかを設定します
開発時には役に立ちますが、内部事情がバレてしまうのでプロダクションではOffにしましょう

### DeepDive

エラーハンドラーのセットアップ

![plantuml (7).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/173064/18b52005-3f15-5ae8-3676-7d3059881e44.png)

エラーハンドラーの呼び出し

![plantuml (8).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/173064/223a86b1-ebf4-9339-193b-6c39e2fba62f.png)


## html_errors

### 説明

display_errorsかlog_errorsが有効な場合に、HTMLタグをエラーメッセージ内に含めることができます
上記の2つのパラメータが無効な場合、html_errorsを有効にしていてもHTMLタグを含んだエラーメッセージは出力されません
こちらのパラメータもプロダクションではOffにしておきましょう

### DeepDive

php_error_cb、php_verrorやphp_error_docref内の条件分岐のパラメータとして使用されます

HTMLタグを含めるかどうかのシンプルな条件分岐で使われている例
https://github.com/php/php-src/blob/PHP-7.3/main/main.c#L1338-L1358

```c:php_error_cb
if (PG(html_errors)) {
	if (type == E_ERROR || type == E_PARSE) {
		zend_string *buf = php_escape_html_entities((unsigned char*)buffer, buffer_len, 0, ENT_COMPAT, get_safe_charset_hint());
		php_printf("%s<br />\n<b>%s</b>:  %s in <b>%s</b> on line <b>%" PRIu32 "</b><br />\n%s", STR_PRINT(prepend_string), error_type_str, ZSTR_VAL(buf), error_filename, error_lineno, STR_PRINT(append_string));
		zend_string_free(buf);
	} else {
		php_printf("%s<br />\n<b>%s</b>:  %s in <b>%s</b> on line <b>%" PRIu32 "</b><br />\n%s", STR_PRINT(prepend_string), error_type_str, buffer, error_filename, error_lineno, STR_PRINT(append_string));
	}
} else {
	/* Write CLI/CGI errors to stderr if display_errors = "stderr" */
	if ((!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "cgi") || !strcmp(sapi_module.name, "phpdbg")) &&
		PG(display_errors) == PHP_DISPLAY_ERRORS_STDERR
	) {
		fprintf(stderr, "%s: %s in %s on line %" PRIu32 "\n", error_type_str, buffer, error_filename, error_lineno);
#ifdef PHP_WIN32
		fflush(stderr);
#endif
	} else {
		php_printf("%s\n%s: %s in %s on line %" PRIu32 "\n%s", STR_PRINT(prepend_string), error_type_str, buffer, error_filename, error_lineno, STR_PRINT(append_string));
	}
}
```

## allow_url_fopen

### 説明

外部のリソース(URL)をファイルオブジェクトのように扱います
fopenのラッパーであるfile_get_contentsなどによってURLにアクセス可能になるので、任意のスクリプトをアプリケーションに挿入できてしまいます
悪意ある外部のスクリプトによってインジェクションされないように、allow_url_fopenは必ず無効化しましょう

### DeepDive

![plantuml (9).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/173064/c8c00f53-73b5-7b0b-c8e9-e3a1cb005af3.png)

実際にallow_url_fopenによってURLにアクセス可能か条件分岐しているコード
https://github.com/php/php-src/blob/PHP-7.3/main/streams/streams.c#L1850-L1866

```c:php_stream_locate_url_wrapper
if (wrapper && wrapper->is_url &&
			(options & STREAM_DISABLE_URL_PROTECTION) == 0 &&
		(!PG(allow_url_fopen) ||
			(((options & STREAM_OPEN_FOR_INCLUDE) ||
				PG(in_user_include)) && !PG(allow_url_include)))) {
	if (options & REPORT_ERRORS) {
		/* protocol[n] probably isn't '\0' */
		if (!PG(allow_url_fopen)) {
			php_error_docref(NULL, E_WARNING, "%.*s:// wrapper is disabled in the server configuration by allow_url_fopen=0", (int)n, protocol);
		} else {
			php_error_docref(NULL, E_WARNING, "%.*s:// wrapper is disabled in the server configuration by allow_url_include=0", (int)n, protocol);
		}
	}
	return NULL;
}

return wrapper;
```

## allow_url_include

### 説明

includeやrequireでURL(file://、http://)をオープンするかを制御する
外部リソースの状態は不明な場合が多く不確実性があるので、allow_url_includeは無効化しましょう
信頼できる外部リソースでもURL経由ではなく、ダウンロードして内部に保存し、検証して安全性を確保された状態のものを使用しましょう

# 終わりに

公式ドキュメントとソースコードを追いながら、実際にパラメータを設定するとどうなるのってところまで深堀してみました
基本的なパラメータしか深掘りできていないので、sessoin周りとかもいずれはDeep Diveしていきたいですね

# Reference

- https://www.php.net/manual/ja/ini.core.php
- https://github.com/php/php-src/blob/PHP-7.3
- https://cheatsheetseries.owasp.org/cheatsheets/PHP_Configuration_Cheat_Sheet.html