ModSecurity with Nginx の注意点 - 設定ファイルの処理順序

## 期待通りに動かなかった点
ModSecurity 2.8.0 で OWASP ModSecurity Core Rule Set (CRS) を使いながら
SecRuleUpdateActionByID しても効かなかったので調査。

下記のように設定していた。(ID は例)
DetectionOnly でも 987173 にマッチすれば落としてくれるはずだったが、落とさなかった。

```modsecurity_crs_10_setup.conf
Include base_rules/*conf
```

```base_rules/modsecurity_crs_99_custom.conf
SecRuleUpdateActionByID 9871173 "ctl:ruleEngine=On"
```


## 結論から言うと、設定ファイルを読む順番が降順になっている
ので、ファイルを分けて定義する時にはファイル名に注意する。
CRS はソートを意識しているであろうファイル名になっているが、これらの順序が重要かは確認していない。
Apache 2.2.x (忘れた) で使った時は昇順に読まれていたようだ。

別ファイルで SecRuleUpdateTargetById のようなことをするなら、その ID でルールが定義されたファイル名より後に処理されるファイル名にする必要があるようだ。

前述の例も、modsecurity_crs_99_custom.conf というファイル名を
99_custom.conf のように変更して reload するとマッチしたリクエストを落としてくれた。

ソースを直したい気もするけど、バージョンアップする時に忘れていそうで怖い。
とりあえず都度ちゃんとテストしましょうとしか言えないか。


## 調査ログ

起動時に strace すると 昇順ソートして stat(2) した後
降順ソートして open(2) しているかのような様子が確認できる。

```
23787 18:04:04.891452 stat("/usr/local/nginx/conf/modsecurity_crs/base_rules/modsecurity_crs_20_protocol_violations.conf", {st_mode=S_IFREG|0644, st_size=22924, ...}) = 0
23787 18:04:04.891499 stat("/usr/local/nginx/conf/modsecurity_crs/base_rules/modsecurity_crs_21_protocol_anomalies.conf", {st_mode=S_IFREG|0644, st_size=6914, ...}) = 0
23787 18:04:04.891545 stat("/usr/local/nginx/conf/modsecurity_crs/base_rules/modsecurity_crs_23_request_limits.conf", {st_mode=S_IFREG|0644, st_size=3792, ...}) = 0
23787 18:04:04.891605 stat("/usr/local/nginx/conf/modsecurity_crs/base_rules/modsecurity_crs_30_http_policy.conf", {st_mode=S_IFREG|0644, st_size=6933, ...}) = 0
23787 18:04:04.891654 stat("/usr/local/nginx/conf/modsecurity_crs/base_rules/modsecurity_crs_35_bad_robots.conf", {st_mode=S_IFREG|0644, st_size=5410, ...}) = 0
23787 18:04:04.891701 stat("/usr/local/nginx/conf/modsecurity_crs/base_rules/modsecurity_crs_40_generic_attacks.conf", {st_mode=S_IFREG|0644, st_size=20874, ...}) = 0
23787 18:04:04.891754 stat("/usr/local/nginx/conf/modsecurity_crs/base_rules/modsecurity_crs_41_sql_injection_attacks.conf", {st_mode=S_IFREG|0644, st_size=44680, ...}) = 0
23787 18:04:04.891801 stat("/usr/local/nginx/conf/modsecurity_crs/base_rules/modsecurity_crs_41_xss_attacks.conf", {st_mode=S_IFREG|0644, st_size=99654, ...}) = 0
23787 18:04:04.891847 stat("/usr/local/nginx/conf/modsecurity_crs/base_rules/modsecurity_crs_42_tight_security.conf", {st_mode=S_IFREG|0644, st_size=1795, ...}) = 0
23787 18:04:04.891893 stat("/usr/local/nginx/conf/modsecurity_crs/base_rules/modsecurity_crs_45_trojans.conf", {st_mode=S_IFREG|0644, st_size=3660, ...}) = 0
23787 18:04:04.891938 stat("/usr/local/nginx/conf/modsecurity_crs/base_rules/modsecurity_crs_47_common_exceptions.conf", {st_mode=S_IFREG|0644, st_size=2247, ...}) = 0
23787 18:04:04.891984 stat("/usr/local/nginx/conf/modsecurity_crs/base_rules/modsecurity_crs_49_inbound_blocking.conf", {st_mode=S_IFREG|0644, st_size=1838, ...}) = 0
23787 18:04:04.892083 stat("/usr/local/nginx/conf/modsecurity_crs/base_rules/modsecurity_crs_50_outbound.conf", {st_mode=S_IFREG|0644, st_size=22336, ...}) = 0
23787 18:04:04.892131 stat("/usr/local/nginx/conf/modsecurity_crs/base_rules/modsecurity_crs_59_outbound_blocking.conf", {st_mode=S_IFREG|0644, st_size=1448, ...}) = 0
23787 18:04:04.892175 stat("/usr/local/nginx/conf/modsecurity_crs/base_rules/modsecurity_crs_60_correlation.conf", {st_mode=S_IFREG|0644, st_size=2674, ...}) = 0
23787 18:04:04.892221 stat("/usr/local/nginx/conf/modsecurity_crs/base_rules/modsecurity_crs_99_custom.conf", {st_mode=S_IFREG|0644, st_size=980, ...}) = 0
23787 18:04:04.892269 open("/usr/local/nginx/conf/modsecurity_crs/base_rules/modsecurity_crs_99_custom.conf", O_RDONLY|O_CLOEXEC) = 8
23787 18:04:04.892560 open("/usr/local/nginx/conf/modsecurity_crs/base_rules/modsecurity_crs_60_correlation.conf", O_RDONLY|O_CLOEXEC) = 8
23787 18:04:04.892993 open("/usr/local/nginx/conf/modsecurity_crs/base_rules/modsecurity_crs_59_outbound_blocking.conf", O_RDONLY|O_CLOEXEC) = 8
23787 18:04:04.893258 open("/usr/local/nginx/conf/modsecurity_crs/base_rules/modsecurity_crs_50_outbound.conf", O_RDONLY|O_CLOEXEC) = 8
23787 18:04:04.894953 open("/usr/local/nginx/conf/modsecurity_crs/base_rules/modsecurity_50_outbound.data", O_RDONLY) = 9
23787 18:04:04.897073 open("/usr/local/nginx/conf/modsecurity_crs/base_rules/modsecurity_crs_49_inbound_blocking.conf", O_RDONLY|O_CLOEXEC) = 8
23787 18:04:04.897384 open("/usr/local/nginx/conf/modsecurity_crs/base_rules/modsecurity_crs_47_common_exceptions.conf", O_RDONLY|O_CLOEXEC) = 8
23787 18:04:04.897872 open("/usr/local/nginx/conf/modsecurity_crs/base_rules/modsecurity_crs_45_trojans.conf", O_RDONLY|O_CLOEXEC) = 8
23787 18:04:04.898302 open("/usr/local/nginx/conf/modsecurity_crs/base_rules/modsecurity_crs_42_tight_security.conf", O_RDONLY|O_CLOEXEC) = 8
23787 18:04:04.898636 open("/usr/local/nginx/conf/modsecurity_crs/base_rules/modsecurity_crs_41_xss_attacks.conf", O_RDONLY|O_CLOEXEC) = 8
23787 18:04:04.911115 open("/usr/local/nginx/conf/modsecurity_crs/base_rules/modsecurity_crs_41_sql_injection_attacks.conf", O_RDONLY|O_CLOEXEC) = 8
23787 18:04:04.915969 open("/usr/local/nginx/conf/modsecurity_crs/base_rules/modsecurity_crs_40_generic_attacks.conf", O_RDONLY|O_CLOEXEC) = 8
23787 18:04:04.917356 open("/usr/local/nginx/conf/modsecurity_crs/base_rules/modsecurity_40_generic_attacks.data", O_RDONLY) = 9
23787 18:04:04.920080 open("/usr/local/nginx/conf/modsecurity_crs/base_rules/modsecurity_crs_35_bad_robots.conf", O_RDONLY|O_CLOEXEC) = 8
23787 18:04:04.920236 open("/usr/local/nginx/conf/modsecurity_crs/base_rules/modsecurity_35_scanners.data", O_RDONLY) = 9
23787 18:04:04.920793 open("/usr/local/nginx/conf/modsecurity_crs/base_rules/modsecurity_35_bad_robots.data", O_RDONLY) = 9
23787 18:04:04.922091 open("/usr/local/nginx/conf/modsecurity_crs/base_rules/modsecurity_crs_30_http_policy.conf", O_RDONLY|O_CLOEXEC) = 8
23787 18:04:04.922764 open("/usr/local/nginx/conf/modsecurity_crs/base_rules/modsecurity_crs_23_request_limits.conf", O_RDONLY|O_CLOEXEC) = 8
23787 18:04:04.923311 open("/usr/local/nginx/conf/modsecurity_crs/base_rules/modsecurity_crs_21_protocol_anomalies.conf", O_RDONLY|O_CLOEXEC) = 8
23787 18:04:04.924086 open("/usr/local/nginx/conf/modsecurity_crs/base_rules/modsecurity_crs_20_protocol_violations.conf", O_RDONLY|O_CLOEXEC) = 8
```


Include ディレクティブの処理は standalone/config.c 内で実装されていた。
apr_dir_read() は No ordering is guaranteed for the entries read. らしいので最初はこれかと思ったが、ソートはしているようだ。

```c:standalone/config.c
    /*
     * first course of business is to grok all the directory
     * entries here and store 'em away. Recall we need full pathnames
     * for this.
     */
    rv = apr_dir_open(&dirp, path, ptemp);
    if (rv != APR_SUCCESS) {
        char errmsg[120];
        return apr_psprintf(p, "Could not open config directory %s: %s",
                            path, apr_strerror(rv, errmsg, sizeof errmsg));
    }

    candidates = apr_array_make(ptemp, 1, sizeof(fnames));
    while (apr_dir_read(&dirent, APR_FINFO_DIRENT | APR_FINFO_TYPE, dirp) == APR_SUCCESS) {
        /* strip out '.' and '..' */
        if (strcmp(dirent.name, ".")
            && strcmp(dirent.name, "..")
            && (apr_fnmatch(fname, dirent.name,
                            APR_FNM_PERIOD | APR_FNM_NOESCAPE | APR_FNM_PATHNAME) == APR_SUCCESS)) {
            const char *full_path = ap_make_full_path(ptemp, path, dirent.name);
            /* If matching internal to path, and we happen to match something
             * other than a directory, skip it
             */
            if (rest && (rv == APR_SUCCESS) && (dirent.filetype != APR_DIR)) {
                continue;
            }
            fnew = (fnames *) apr_array_push(candidates);
            fnew->fname = full_path;
        }
    }

    apr_dir_close(dirp);
    if (candidates->nelts != 0) {
        const char *error;

        qsort((void *) candidates->elts, candidates->nelts,
              sizeof(fnames), fname_alphasort);
```

```c:standalone/config.c
static int fname_alphasort(const void *fn1, const void *fn2)
{
    const fnames *f1 = fn1;
    const fnames *f2 = fn2;

    return strcmp(f1->fname,f2->fname);
}
```

しかし取り出す時に apr_array_pop() で後ろから読んでるので、結果として処理順序は降順になっていたようだ。

```c:standalone/config.c
    errmsg = populate_include_files(p, ptemp, ari, filename, 0);

    if(errmsg != NULL)
        goto Exit;

    while(ari->nelts != 0 || arr->nelts != 0)
    {
        if(ari->nelts > 0)
        {
            char *fn = *(char **)apr_array_pop(ari);

            parms = (cmd_parms *)apr_array_push(arr);
            *parms = default_parms;
            parms->pool = p;
            parms->temp_pool = ptemp;
            parms->server = s;
            parms->override = (RSRC_CONF | OR_ALL) & ~(OR_AUTHCFG | OR_LIMIT);
            parms->override_opts = OPT_ALL | OPT_SYM_OWNER | OPT_MULTI;

            status = ap_pcfg_openfile(&parms->config_file, p, fn);
```


## 参考文献

* http://apr.apache.org/docs/apr/1.4/index.html
* https://github.com/SpiderLabs/ModSecurity/wiki/Reference-Manual