レスポンス・ヘッダフィールドからnginxのバージョンを消す...から学べること

f:id:kumamon_engineer:20200928214516p:plain

 

こんにちは、もうすっかり秋ですね。エンジニアの三宅です。

秋といえばnginxですね。

 今回は、nginxのバージョンがHTTPのレスポンス・ヘッダフィールドにdefaultで設定されるという噂を耳にしたので確認します。

 

 

nginxのバージョン設定

nginxに特に設定をしていないとHTTPのレスポンスヘッダーにサーバーアプリケーションのバージョンが表示されてしまいます。

f:id:kumamon_engineer:20200928212914p:plain

あ、見えちゃってる…!
 
レスポンスヘッダーにアプリケーションのバージョンがあると何が良くないかというのは、HTTPのRFCには以下の記述があります。

tools.ietf.org

 
      Note: Revealing the specific software version of the server might
      allow the server machine to become more vulnerable to attacks
      against software that is known to contain security holes. Server
      implementors are encouraged to make this field a configurable
      option.
 
      注意: サーバーの特定のソフトウェアバージョンを明らかにすると、
      以下のようになる場合があります。
      サーバマシンが攻撃に対してより脆弱になることを可能にします。
      セキュリティホールが含まれていることが知られているソフトウェア
       に対してサーバ実装者は、このフィールドを設定可能なオプションを
       使用します。

 

特定のバージョンで動いている事を公開していることは、セキュリティホールが含まれたバージョンを使っていた場合、攻撃対象になってしまいます…ということですね。

まぁ、見せて得することはあまり無さそうですね。

 

nginxの方ではconfig設定で以下を定義する事で消すことが出来るそうです。

 

Syntax: server_tokens on | off | build | string;
Default: server_tokens on;
Context: http, server, location
 
Enables or disables emitting nginx version on error pages and in the “Server” response header field.

conf用ファイル
http {
    server_tokens off;
    ...
}
 
これでnginxを再起動
 
はい、消えました!

f:id:kumamon_engineer:20200928213019p:plain

ありがとうございました!お疲れさまでした!
 
 
 
 
 
fin...?
 
 
 
 
さて、これってnginxのソフトウェア上では何が起きてるのでしょうか..。
今回はnginxのコードを読んで理解を深めていこうと思います。
 

nginxのコードを読む

nginxのコードはGithubで見れます!ただここで管理されている訳ではなく、
公式のコードを1時間おきに更新しているミラーです。
参照するには十分ですね。
 
さて、nginxはC言語で書かれていますので、コードを読むには最低限のC言語の知識は必要になります。

main関数のループ

まずはmain関数を確認します!
 
ありました、ありました!
C言語のソフトウェアはエントリーポイントとしてmain関数が呼ばれます。
この関数抜けるとソフトウェアが終了してしまうため、どこかに無限ループがあるはずです。
 
ここでプロセス管理してそうですね
 
if (ngx_process == NGX_PROCESS_SINGLE) {
 
ngx_single_process_cycle(cycle);
 
 
} else {
 
ngx_master_process_cycle(cycle);
}
 
さて、 ngx_master_process_cycle 関数を見てみます。
C言語では関数を呼び出すためにはheaderファイルで定義したものをincludeすることで出来ます。ngx_master_process_cycleが定義されているのはngx_process_cycle.hで、これはngx_core.hでincludeされており、nginx.cではngx_core.hをincludeしています。
 
あれngx_process_cycle.hが2つあるな…

f:id:kumamon_engineer:20200928214007p:plain

ngx_process_cycle.hファイルはOSがunix系かwindows系かで分かれるようですね。OSによって中の実装が変わるシステムコール系の処理をラッピングする為に、同名でOS毎に分けて書かれているんですね

 
どっちが呼び出されるか…
UNIX_DEPSとして定義】
UNIX_DEPS="$CORE_DEPS $EVENT_DEPS \
            src/os/unix/ngx_time.h \
            src/os/unix/ngx_errno.h \
            src/os/unix/ngx_alloc.h \
            src/os/unix/ngx_files.h \
            src/os/unix/ngx_channel.h \
            src/os/unix/ngx_shmem.h \
            src/os/unix/ngx_process.h \
            src/os/unix/ngx_setaffinity.h \
            src/os/unix/ngx_setproctitle.h \
            src/os/unix/ngx_atomic.h \
            src/os/unix/ngx_gcc_atomic_x86.h \
            src/os/unix/ngx_thread.h \
            src/os/unix/ngx_socket.h \
            src/os/unix/ngx_os.h \
            src/os/unix/ngx_user.h \
            src/os/unix/ngx_dlopen.h \
            src/os/unix/ngx_process_cycle.h"
【WIN32_DEPSとして定義】
https://github.com/nginx/nginx/blob/38196b8ba63f04830db0b8793eca738e162c6d8e/auto/sources#L228
WIN32_DEPS="$CORE_DEPS $EVENT_DEPS \
            src/os/win32/ngx_win32_config.h \
            src/os/win32/ngx_time.h \
            src/os/win32/ngx_errno.h \
            src/os/win32/ngx_alloc.h \
            src/os/win32/ngx_files.h \
            src/os/win32/ngx_shmem.h \
            src/os/win32/ngx_process.h \
            src/os/win32/ngx_atomic.h \
            src/os/win32/ngx_thread.h \
            src/os/win32/ngx_socket.h \
            src/os/win32/ngx_os.h \
            src/os/win32/ngx_user.h \
            src/os/win32/ngx_dlopen.h \
            src/os/win32/ngx_process_cycle.h"

この設定はmakeファイルにあるはずです。

C言語はmakeコマンドでコンパイルする場合、makeファイルにコンパイル設定を定義します。
 
ここの配下でOS毎の定義をまとめています

f:id:kumamon_engineer:20200929092257p:plain

OS毎に色々やってますね
case "$NGX_PLATFORM" in
FreeBSD:*)   . auto/os/freebsd   ;;    Linux:*)   . auto/os/linux   ;;   SunOS:*)   . auto/os/solaris   ;; Darwin:*)   . auto/os/darwin   ;;   win32)   . auto/os/win32   ;; 

 

windowsはここでWIN32_DEPSを設定

https://github.com/nginx/nginx/blob/c85d6fec217d1b17291779542de20ad77ae68661/auto/os/win32#L9

CORE_DEPS="$WIN32_DEPS"

LinuxはここでUNIX_DEPSを設定

https://github.com/nginx/nginx/blob/c85d6fec217d1b17291779542de20ad77ae68661/auto/os/linux#L9

CORE_DEPS="$UNIX_DEPS $LINUX_DEPS"

Solarisだったらここ(LinuxSolarisUNIX系なのでUNIX_DEPSは共通ですね)

https://github.com/nginx/nginx/blob/c85d6fec217d1b17291779542de20ad77ae68661/auto/os/solaris#L9

CORE_DEPS="$UNIX_DEPS $SOLARIS_DEPS"

 makeファイルでCORE_DEPSを利用

https://github.com/nginx/nginx/blob/c85d6fec217d1b17291779542de20ad77ae68661/auto/make#L56

ngx_deps=`echo $CORE_DEPS $NGX_AUTO_CONFIG_H $NGX_PCH \
| sed -e "s/ *\([^ ][^ ]*\)/$ngx_regex_cont\1/g" \
-e "s/\//$ngx_regex_dirsep/g"`
 

…ということで、ngx_master_process_cycleはUNIX系かWindows系かで別れていますよ…と。

 

差分を見ると関数の中身、初っ端から全然違いますね。。

f:id:kumamon_engineer:20200929083312p:plain

windows(左側)はHANDLEとか使ってますね。

windowsでスレッドやイベント生成した時に返ってくるポインタの型ですね。

windowsプログラムの基本ですね、懐かしい

(そして、今はweb上にこんなドキュメントがあるのか...羨ましい)

docs.microsoft.com

 

そうそうmain関数の中にある無限ループを探すのが目的でした…

 

ありました!このfor文の中をグルグル回ってるんですね、nginxは..

https://github.com/nginx/nginx/blob/9c3ac44de268f0cf057bc5dd67929e74c9bbc3e3/src/os/unix/ngx_process_cycle.c#L139

for ( ;; ) {
 ・・・
}

本筋を見失ってしまいましたが、この無限ループ内でworkerプロセスのイベント監視して非同期にhttp通信を管理しているんですね。。。

 

アーキテクチャはこの図がわかりやすいです!

f:id:kumamon_engineer:20200929055307p:plain

 responseに設定しているのは何処

では今回のレスポンス・ヘッダーにサーバーアプリケーションのバージョンを設定している部分はソフトウェアではどうしているのでしょうか。

  

まずは、NGINXのバージョンを定義している部分を見つけました

https://github.com/nginx/nginx/blob/554916301c424f02b1cabc073845b64f8681099b/src/core/nginx.h#L14

#define NGINX_VERSION "1.19.3"
#define NGINX_VER "nginx/" NGINX_VERSION

恐らく、この定義を元に設定している部分があるのでしょう

 

ありますね!ngx_http_header_filter_module.cファイルですね。ヘッダーを組み立てる処理が書かれていそうです。 

https://github.com/nginx/nginx/blob/4bf4650f2f10f7bbacfe7a33da744f18951d416d/src/http/ngx_http_header_filter_module.c#L50

static u_char ngx_http_server_string[] = "Server: nginx" CRLF;
static u_char ngx_http_server_full_string[] = "Server: " NGINX_VER CRLF;
static u_char ngx_http_server_build_string[] = "Server: " NGINX_VER_BUILD CRLF;

ngx_http_server_full_stringがversion付きの文字列ですね

ここで使ってますね

https://github.com/nginx/nginx/blob/4bf4650f2f10f7bbacfe7a33da744f18951d416d/src/http/ngx_http_header_filter_module.c#L452

        if (clcf->server_tokens == NGX_HTTP_SERVER_TOKENS_ON) {
            p = ngx_http_server_full_string;
            len = sizeof(ngx_http_server_full_string) - 1;

        } else if (clcf->server_tokens == NGX_HTTP_SERVER_TOKENS_BUILD) {
            p = ngx_http_server_build_string;
            len = sizeof(ngx_http_server_build_string) - 1;

        } else {
            p = ngx_http_server_string;
            len = sizeof(ngx_http_server_string) - 1;
        }

        b->last = ngx_cpymem(b->last, p, len);

if (clcf->server_tokens == NGX_HTTP_SERVER_TOKENS_ON) { で判定されてますね。b->lastにcopyされてます。

ちなみにngx_cpymemmemcpyをラッピングしたマクロになります。
https://github.com/nginx/nginx/blob/6c3838f9ed45f5c2aa6a971a0da3cb6ffe45b61e/src/core/ngx_string.h#L107

#define ngx_cpymem(dst, src, n)   (((u_char *) memcpy(dst, src, n)) + (n))

clcf はなんでしょう。
https://github.com/nginx/nginx/blob/4bf4650f2f10f7bbacfe7a33da744f18951d416d/src/http/ngx_http_header_filter_module.c#L280

clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);

関数名を見る限り、ローカルの設定のようです。

構造体の定義struct ngx_http_core_loc_conf_sはここにあります。
設定を集約した定義のようです。https://github.com/nginx/nginx/blob/b82c08f6102d65a5e5902e6fa85082e184a75003/src/http/ngx_http_core_module.h#L397

struct ngx_http_core_loc_conf_s {
  ...
    ngx_uint_t    server_tokens;           /* server_tokens */
  ...
}

初期値はONなのか? 

https://github.com/nginx/nginx/blob/b82c08f6102d65a5e5902e6fa85082e184a75003/src/http/ngx_http_core_module.c#L3794

ngx_conf_merge_uint_value(conf->server_tokens, prev->server_tokens,
                              NGX_HTTP_SERVER_TOKENS_ON);

// 定義
#define ngx_conf_merge_value(conf, prev, default)                            \
    if (conf == NGX_CONF_UNSET) {                                            \
        conf = (prev == NGX_CONF_UNSET) ? default : prev;                    \
    }

確かにdefaultはONで設定されていました!

offをconfigに設定するとNGX_HTTP_SERVER_TOKENS_OFFが設定されることも確認できます。

https://github.com/nginx/nginx/blob/b82c08f6102d65a5e5902e6fa85082e184a75003/src/http/ngx_http_core_module.c#L125

static ngx_conf_enum_t  ngx_http_core_server_tokens[] = {
    { ngx_string("off"), NGX_HTTP_SERVER_TOKENS_OFF },
    { ngx_string("on"), NGX_HTTP_SERVER_TOKENS_ON },
    { ngx_string("build"), NGX_HTTP_SERVER_TOKENS_BUILD },
    { ngx_null_string, 0 }
};

ということで...

http {
    server_tokens off;
    ...
}

を設定すると、condig設定でoff→NGX_HTTP_SERVER_TOKENS_OFFに置き換わり、header_filter_moduleの分岐にてどの文字列をcopyするかが決まります。 

response送信

最後、送信するところですね

https://github.com/nginx/nginx/blob/b82c08f6102d65a5e5902e6fa85082e184a75003/src/http/ngx_http_core_module.c#L1733

ngx_int_t
ngx_http_send_response(ngx_http_request_t *r, ngx_uint_t status,
    ngx_str_t *ct, ngx_http_complex_value_t *cv)
{
   ...
        rc = ngx_http_send_header(r);
   ...
}

// ヘッダー生成
ngx_int_t
ngx_http_send_header(ngx_http_request_t *r)
{
    if (r->post_action) {
        return NGX_OK;
    }

    if (r->header_sent) {
        ngx_log_error(NGX_LOG_ALERT, r->connection->log, 0,
                      "header already sent");
        return NGX_ERROR;
    }

    if (r->err_status) {
        r->headers_out.status = r->err_status;
        r->headers_out.status_line.len = 0;
    }

    return ngx_http_top_header_filter(r);
}

ngx_http_send_response でresponseを生成しています。
ngx_http_send_headerでヘッダー部分を作ります。
その中でngx_http_top_header_filterを呼び出しています。

ngx_http_top_header_filterは以下で設定されます。

https://github.com/nginx/nginx/blob/4bf4650f2f10f7bbacfe7a33da744f18951d416d/src/http/ngx_http_header_filter_module.c#L625

static ngx_int_t
ngx_http_header_filter_init(ngx_conf_t *cf)
{
    ngx_http_top_header_filter = ngx_http_header_filter;

    return NGX_OK;
}

ここで ngx_http_top_header_filter に ngx_http_header_filter を設定しています。ngx_http_header_filterは上記のngx_cpymemで設定してる関数になります。
ここでヘッダーの設定部分と実際にresponseを生成している関数が繋がります。

 

おわりに

という事でserver_tokens off;の設定から、nginxが実際に設定する所を読んでみました。オープンソースは気になったら実際にコードを読み、どんな挙動をしているか明確にわかるのが良いですね。また、そのアーキテクチャ含め、コードから色んな事を学べることが大きな利点だと思います。

また機会があればこういう記事を書いていこうと思います。