Help us understand the problem. What is going on with this article?

【PHP】php.ini Security Deep Dive

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

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

前提

php.ini

php.iniで設定可能な全パラメータは公式ドキュメントPHP-7.3/php.ini-productionを参照ください
では、セキュリティの観点から設定した方がいいパラメータについて説明していきます

open_basedir

説明

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

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

DeepDive

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

plantuml (1).png

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

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に1を指定しているので、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

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

/* 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

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

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

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

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

display_errors

説明

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

DeepDive

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

plantuml (7).png

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

plantuml (8).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

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

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

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

homines22
Network programmingやインフラ(サーバー・ネットワーク)などに興味があります。 最近はAWSを使ってサービス開発しています。 よく使う言語はC / Python / TypeScript / Rustです。
hands-lab
ハンズラボは小売業特化型ITソリューション企業です。数十万に及ぶ膨大な商品マスタを扱ってきた豊富なノウハウで、お客様の現場に最適なシステムを提案・開発します。 エンジニア募集中
https://www.hands-lab.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away