【PHP】php.ini 安全深入研究
各位的PHPer朋友们,你们都正确设置了php.ini吗?不会全都使用默认值吧?(挑衅)
我想要在安全性的角度下,介绍一些最好设置的参数值,并尽可能详细地解释它们。
前提
php.ini的意思是PHP配置文件。
请参考官方文档和PHP-7.3/php.ini-production,这里列举了可以在php.ini中设置的所有参数。
现在我们将开始说明出于安全性考虑而应设置的参数。
打开基目录
解释
为了防止directory traversal等攻击,应该始终设置PHP限制可访问的目录。默认情况下,所有目录都是可访问的。
在许多情况下,只需指定/var/www/html即可,但也可以使用冒号作为分隔符来指定多个目录。
深度挖掘
让我们跟踪open_basedir的实施。

以下是抽取自php_check_open_basedir_ex核心处理的代码。
https://github.com/php/php-src/blob/PHP-7.3/main/fopen_wrappers.c#L300-L319
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函数的返回值为NULL(表示根据open_basedir的设置值结束搜索),就会循环执行。
如果在php_check_open_basedir函数中调用时,给第二个参数warn指定了1,那么当尝试调用未在open_basedir指定的目录时,将通过php_error_docref输出警告。
将警告参数传递,并根据在条件分支之前返回与否决定是否发出警告,这是一种很好的实现方法。
不需要在内部操作警告标志,显得清爽简洁。
由于php_check_specific_open_basedir对符号链接也进行了支持,所以如果文件本身位于open_basedir之外,它会报错。以下是处理符号链接的代码:https://github.com/php/php-src/blob/PHP-7.3/main/fopen_wrappers.c#L167-L181
#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
/* 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 以禁用
简单而美丽(只是想说一下w
禁用函数
解释
你可以设置禁止执行的函数。
考虑到通常应用程序不会操作文件系统或操作系统用户,我们应该禁止可以进行这些更改的函数。
作为设置disable_functions的建议,最初可以设置得严格一些,然后通过测试等逐步从设置中删除需要执行的函数。
需要注意的是,这会影响到除了你自己编写的代码之外的库和框架等。
由于一行看起来不太清晰,我将其列成列表,但是在写入php.ini时,请将逗号作为分隔符写在一行中。
- 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である関数)
深潜
我们来追踪disable_functions的实现。

disable_functions在PHP模块启动时加载,这意味着它不能被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
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中搜索要禁用的函数,如果找到相应的函数,则将其禁用。
显示PHP
解释
将 expose_php 设为 Off,可以防止将 PHP 处理和版本信息包含在 HTTP Header 中。这在生产环境中是必须关闭的参数。
深潜

HTTP头部的设置内容如下所定义:https://github.com/php/php-src/blob/PHP-7.3/main/SAPI.h#L289
#define SAPI_PHP_VERSION_HEADER "X-Powered-By: PHP/" PHP_VERSION
显示错误
说明
设置是否将运行时的PHP错误作为HTML的一部分输出到屏幕上。
在开发阶段很有用,但因泄漏内部信息而在生产环境中关闭。
深潜探秘
エラーハンドラーのセットアップ

调用错误处理程序

html_errors
説明
display_errorsかlog_errorsが有効な場合に、HTMLタグをエラーメッセージ内に含めることができます
上記の2つのパラメータが無効な場合、html_errorsを有効にしていてもHTMLタグを含んだエラーメッセージは出力されません
こちらのパラメータもプロダクションではOffにしておきましょう
深潜
php_error_cb、php_verror和php_error_docref中使用的条件分支参数。
用于简单的条件分支来判断是否包括 HTML 标签的示例。
https://github.com/php/php-src/blob/PHP-7.3/main/main.c#L1338-L1358
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));
}
}
允许打开URL
解释
外部のリソース(URL)をファイルオブジェクトのように扱います
fopenのラッパーであるfile_get_contentsなどによってURLにアクセス可能になるので、任意のスクリプトをアプリケーションに挿入できてしまいます
悪意ある外部のスクリプトによってインジェクションされないように、allow_url_fopenは必ず無効化しましょう
DeepDive

実際にallow_url_fopenによってURLにアクセス可能か条件分岐しているコード
https://github.com/php/php-src/blob/PHP-7.3/main/streams/streams.c#L1850-L1866
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;
允许网址包含
説明
includeやrequireでURL(file://、http://)をオープンするかを制御する
外部リソースの状態は不明な場合が多く不確実性があるので、allow_url_includeは無効化しましょう
信頼できる外部リソースでもURL経由ではなく、ダウンロードして内部に保存し、検証して安全性を確保された状態のものを使用しましょう
结束
我一边追踪公式文档和源代码,一边实际设置参数,看看会发生什么。
由于只能深入了解基本参数,所以以后还想深入研究会话等其他方面。
参考文献
-
- 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