Apache WAF mod_security の導入

# 想定環境
CentOS 6 または 7

```shell-session
# rpm -q mod_security mod_security_crs
mod_security-2.7.3-5.el6.x86_64
mod_security_crs-2.2.6-3.el6.noarch

# rpm -q mod_security mod_security_crs
mod_security-2.9.2-1.el7.x86_64
mod_security_crs-2.2.9-1.el7.noarch
```

# はじめに、導入方針など

- デフォルトでインストールされるルールでは、誤検知が発生するので、最小限のルールのみ有効にします。
- 現状、本番環境での初期導入時はSQLインジェクションとクロスサイトスクリプティングに関するルールのみ残し、他は無効にします。
- 最初は、検出オンリーモードで様子をみるのが良さそうです。
- 開発環境では全てのルールを有効にしてみるなど、いろいろ試してみてください。問題ないと確認できれば、本番環境で随時ルールを有効にしていきます。
- デフォルトではログ出力が多いので、ディスク容量に余裕のない環境では設定を調整してください。
- 本番環境での誤検知発生にそなえて、特定のURLでルールを無効にする方法を確認してください。

# rpmパッケージの導入

epelリポジトリからrpmパッケージをインストールします。

```shell-session
# yum -y install mod_security mod_security_crs
```

`mod_security` が Apache モジュール、`mod_security_crs` がWAFのルールです。

初回導入時は、必ず設定を調整してから Apache を再起動して反映します。

```shell-session
# service httpd restart
```

設定変更時は、リロードで反映します。

```shell-session
# service httpd reload
```

# SQLインジェクション・クロスサイトスクリプティングのルールを残し、その他は無効化する

デフォルトで読み込まれるWAFルールは、`/etc/httpd/modsecurity.d/activated_rule` 下にあります。
あらかじめバックアップを取得しておきましょう。

```shell-session
# cd /etc/httpd/modsecurity.d
# cp -rp activated_rule activated_rule.orig
# cd activated_rule
# ls -l
total 88
lrwxrwxrwx 1 root root 64 Jul  3 16:21 modsecurity_35_bad_robots.data -> /usr/lib/modsecurity.d/base_rules/modsecurity_35_bad_robots.data
lrwxrwxrwx 1 root root 62 Jul  3 16:21 modsecurity_35_scanners.data -> /usr/lib/modsecurity.d/base_rules/modsecurity_35_scanners.data
lrwxrwxrwx 1 root root 69 Jul  3 16:21 modsecurity_40_generic_attacks.data -> /usr/lib/modsecurity.d/base_rules/modsecurity_40_generic_attacks.data
lrwxrwxrwx 1 root root 75 Jul  3 16:21 modsecurity_41_sql_injection_attacks.data -> /usr/lib/modsecurity.d/base_rules/modsecurity_41_sql_injection_attacks.data
lrwxrwxrwx 1 root root 62 Jul  3 16:21 modsecurity_50_outbound.data -> /usr/lib/modsecurity.d/base_rules/modsecurity_50_outbound.data
lrwxrwxrwx 1 root root 70 Jul  3 16:21 modsecurity_50_outbound_malware.data -> /usr/lib/modsecurity.d/base_rules/modsecurity_50_outbound_malware.data
lrwxrwxrwx 1 root root 77 Jul  3 16:21 modsecurity_crs_20_protocol_violations.conf -> /usr/lib/modsecurity.d/base_rules/modsecurity_crs_20_protocol_violations.conf
lrwxrwxrwx 1 root root 76 Jul  3 16:21 modsecurity_crs_21_protocol_anomalies.conf -> /usr/lib/modsecurity.d/base_rules/modsecurity_crs_21_protocol_anomalies.conf
lrwxrwxrwx 1 root root 72 Jul  3 16:21 modsecurity_crs_23_request_limits.conf -> /usr/lib/modsecurity.d/base_rules/modsecurity_crs_23_request_limits.conf
lrwxrwxrwx 1 root root 69 Jul  3 16:21 modsecurity_crs_30_http_policy.conf -> /usr/lib/modsecurity.d/base_rules/modsecurity_crs_30_http_policy.conf
lrwxrwxrwx 1 root root 68 Jul  3 16:21 modsecurity_crs_35_bad_robots.conf -> /usr/lib/modsecurity.d/base_rules/modsecurity_crs_35_bad_robots.conf
lrwxrwxrwx 1 root root 73 Jul  3 16:21 modsecurity_crs_40_generic_attacks.conf -> /usr/lib/modsecurity.d/base_rules/modsecurity_crs_40_generic_attacks.conf
lrwxrwxrwx 1 root root 79 Jul  3 16:21 modsecurity_crs_41_sql_injection_attacks.conf -> /usr/lib/modsecurity.d/base_rules/modsecurity_crs_41_sql_injection_attacks.conf
lrwxrwxrwx 1 root root 69 Jul  3 16:21 modsecurity_crs_41_xss_attacks.conf -> /usr/lib/modsecurity.d/base_rules/modsecurity_crs_41_xss_attacks.conf
lrwxrwxrwx 1 root root 72 Jul  3 16:21 modsecurity_crs_42_tight_security.conf -> /usr/lib/modsecurity.d/base_rules/modsecurity_crs_42_tight_security.conf
lrwxrwxrwx 1 root root 65 Jul  3 16:21 modsecurity_crs_45_trojans.conf -> /usr/lib/modsecurity.d/base_rules/modsecurity_crs_45_trojans.conf
lrwxrwxrwx 1 root root 75 Jul  3 16:21 modsecurity_crs_47_common_exceptions.conf -> /usr/lib/modsecurity.d/base_rules/modsecurity_crs_47_common_exceptions.conf
lrwxrwxrwx 1 root root 82 Jul  3 16:21 modsecurity_crs_48_local_exceptions.conf.example -> /usr/lib/modsecurity.d/base_rules/modsecurity_crs_48_local_exceptions.conf.example
lrwxrwxrwx 1 root root 74 Jul  3 16:21 modsecurity_crs_49_inbound_blocking.conf -> /usr/lib/modsecurity.d/base_rules/modsecurity_crs_49_inbound_blocking.conf
lrwxrwxrwx 1 root root 66 Jul  3 16:21 modsecurity_crs_50_outbound.conf -> /usr/lib/modsecurity.d/base_rules/modsecurity_crs_50_outbound.conf
lrwxrwxrwx 1 root root 75 Jul  3 16:21 modsecurity_crs_59_outbound_blocking.conf -> /usr/lib/modsecurity.d/base_rules/modsecurity_crs_59_outbound_blocking.conf
lrwxrwxrwx 1 root root 69 Jul  3 16:21 modsecurity_crs_60_correlation.conf -> /usr/lib/modsecurity.d/base_rules/modsecurity_crs_60_correlation.conf
```

拡張子 .data は、WAFルールが参照する定数などが定義されています。.conf がWAFルールの定義です。

SQLインジェクション`modsecurity_crs_41_sql_injection_attacks.conf`、クロスサイトスクリプティング`modsecurity_crs_41_xss_attacks.conf`を除いて、他のconfファイルを無効化します。
単純にシンボリックリンクを削除すると、yumアップデート時に再度有効になってしまうので、同名の空ファイルに置き換えます。

```shell-session
# find . -type l -name '*_crs_*' | grep -Ev 'sql|xss' | xargs -I% echo rm % \; touch % | bash
# ls -l
total 48
lrwxrwxrwx 1 root root  64 Jul  3 19:56 modsecurity_35_bad_robots.data -> /usr/lib/modsecurity.d/base_rules/modsecurity_35_bad_robots.data
lrwxrwxrwx 1 root root  62 Jul  3 19:56 modsecurity_35_scanners.data -> /usr/lib/modsecurity.d/base_rules/modsecurity_35_scanners.data
lrwxrwxrwx 1 root root  69 Jul  3 19:56 modsecurity_40_generic_attacks.data -> /usr/lib/modsecurity.d/base_rules/modsecurity_40_generic_attacks.data
lrwxrwxrwx 1 root root  75 Jul  3 19:56 modsecurity_41_sql_injection_attacks.data -> /usr/lib/modsecurity.d/base_rules/modsecurity_41_sql_injection_attacks.data
lrwxrwxrwx 1 root root  62 Jul  3 19:56 modsecurity_50_outbound.data -> /usr/lib/modsecurity.d/base_rules/modsecurity_50_outbound.data
lrwxrwxrwx 1 root root  70 Jul  3 19:56 modsecurity_50_outbound_malware.data -> /usr/lib/modsecurity.d/base_rules/modsecurity_50_outbound_malware.data
-rw-r--r-- 1 root root   0 Jul  3 19:56 modsecurity_crs_20_protocol_violations.conf
-rw-r--r-- 1 root root   0 Jul  3 19:54 modsecurity_crs_21_protocol_anomalies.conf
-rw-r--r-- 1 root root   0 Jul  3 19:54 modsecurity_crs_23_request_limits.conf
-rw-r--r-- 1 root root   0 Jul  3 19:54 modsecurity_crs_30_http_policy.conf
-rw-r--r-- 1 root root   0 Jul  3 19:54 modsecurity_crs_35_bad_robots.conf
-rw-r--r-- 1 root root   0 Jul  3 19:54 modsecurity_crs_40_generic_attacks.conf
lrwxrwxrwx 1 root root  79 Jul  3 19:56 modsecurity_crs_41_sql_injection_attacks.conf -> /usr/lib/modsecurity.d/base_rules/modsecurity_crs_41_sql_injection_attacks.conf
lrwxrwxrwx 1 root root  69 Jul  3 19:56 modsecurity_crs_41_xss_attacks.conf -> /usr/lib/modsecurity.d/base_rules/modsecurity_crs_41_xss_attacks.conf
-rw-r--r-- 1 root root   0 Jul  3 19:54 modsecurity_crs_42_tight_security.conf
-rw-r--r-- 1 root root   0 Jul  3 19:54 modsecurity_crs_45_trojans.conf
-rw-r--r-- 1 root root   0 Jul  3 19:54 modsecurity_crs_47_common_exceptions.conf
-rw-r--r-- 1 root root   0 Jul  3 19:54 modsecurity_crs_48_local_exceptions.conf.example
-rw-r--r-- 1 root root   0 Jul  3 19:54 modsecurity_crs_49_inbound_blocking.conf
-rw-r--r-- 1 root root   0 Jul  3 19:54 modsecurity_crs_50_outbound.conf
-rw-r--r-- 1 root root   0 Jul  3 19:54 modsecurity_crs_59_outbound_blocking.conf
-rw-r--r-- 1 root root   0 Jul  3 19:54 modsecurity_crs_60_correlation.conf
```

無効にしたルールを有効にするには、シンボリックリンクを復活させてください。

```shell-session
# ln -sf /usr/lib/modsecurity.d/base_rules/modsecurity_crs_30_http_policy.conf .
```

バックアップから戻してもいいでしょう。

```shell-session
# cp -P ../activated_rules.orig/modsecurity_crs_30_http_policy.conf .
```

デフォルトに戻すには、バックアップした activated_rules に戻します。

```shell-session
# cd /etc/httpd/modsecurity.d
# mv activated_rule activated_rule.bk && mv activated_rule.orig activated_rule
```

誤って上書き・削除してしまったなどデフォルトの状態に戻したい場合は、上書きしたファイルを削除してから `mod_security_crs` パッケージを再インストールします。

```shell-session
# rm /usr/lib/modsecurity.d/base_rules/*.conf
# yum reinstall mod_security_crs
```

# ログ出力の調整

ログはデフォルトで Apache の `error_log` と `/var/log/httpd/modsec_audit.log` に出力されます。

`modsec_audit.log` には、WAFに関係なく404を除く4xx,5xxエラーの詳細な情報が出力されるので、ログが短時間で肥大化しやすく、ディスク容量に余裕がない環境では危険です。

`error_log`だけで検知された事は判断できるので、心配な環境では通常`modsec_audit.log`への出力をオフにしておき、必要なタイミングでオンにすると良いでしょう。

```/etc/httpd/conf.d/mod_security.conf
    SecAuditEngine Off
```

# 動作確認

SQLインジェクション検知の確認は、GETパラメータに`?select+union`を付けてアクセスしてみて、403 Forbidden となるか確認します。
例: https://example.com/hoge/?union+select

`error_log`の出力例を挙げます。

```
[Thu Jul 05 13:57:50 2018] [error] [client 192.168.0.100] ModSecurity: Access denied with code 403 (phase 2). Operator GE matched 3 at TX:sqli_select_statement_count. [file "/etc/httpd/modsecurity.d/activated_rules/modsecurity_crs_41_sql_injection_attacks.conf"] [line "108"] [id "981317"] [rev "2"] [msg "SQL SELECT Statement Anomaly Detection Alert"] [data "Matched Data: Connection found within TX:sqli_select_statement_count: 3"] [ver "OWASP_CRS/2.2.6"] [maturity "8"] [accuracy "8"] [tag "OWASP_CRS/WEB_ATTACK/SQL_INJECTION"] [tag "WASCTC/WASC-19"] [tag "OWASP_TOP_10/A1"] [tag "OWASP_AppSensor/CIE1"] [tag "PCI/6.5.2"] [hostname "example.com"] [uri "/hoge/"] [unique_id "Wz2lTsCoAa4AAEuRGe0AAAAA"]
```

WAFの検知があったかどうかは、文字列'ModSecurity: Access denied with code 'をgrepすれば調査できます。

```shell-session
# grep 'ModSecurity: Access denied with code ' /var/log/httpd/ssl_error_log
```

`[name "value"]` の形式で検知情報が出力されています。適当に改行コードを入れます。

```shell-session
# grep 'ModSecurity: Access denied with code ' /var/log/httpd/ssl_error_log | tail -1 | sed 's/ \[/\n[/g' 
[Thu Jul 05 13:57:50 2018]
[error]
[client 192.168.0.100] ModSecurity: Access denied with code 403 (phase 2). Operator GE matched 3 at TX:sqli_select_statement_count.
[file "/etc/httpd/modsecurity.d/activated_rules/modsecurity_crs_41_sql_injection_attacks.conf"]
[line "108"]
[id "981317"]
[rev "2"]
[msg "SQL SELECT Statement Anomaly Detection Alert"]
[data "Matched Data: Connection found within TX:sqli_select_statement_count: 3"]
[ver "OWASP_CRS/2.2.6"]
[maturity "8"]
[accuracy "8"]
[tag "OWASP_CRS/WEB_ATTACK/SQL_INJECTION"]
[tag "WASCTC/WASC-19"]
[tag "OWASP_TOP_10/A1"]
[tag "OWASP_AppSensor/CIE1"]
[tag "PCI/6.5.2"]
[hostname "example.com"]
[uri "/hoge/"]
[unique_id "Wz2lTsCoAa4AAEuRGe0AAAAA"]

```

ここで重要なのは、`[id "981317"]`です。このルールを無効化する際に SecRuleRemoveById に指定します。

- id: 検知ルールID
- client: 検知されたクライアントIP
- file,line: 検知ルール定義場所
- msg: 検知ルールの概要
- data: 検知された問題のデータ
- hostname,uri: 検知されたURL

クロスサイトスクリプティングは、適当なテキストフォームに`javascript:alert(document.cookie)`を入力してサブミットし、403 Forbidden となるか確認します。
`error_log`の出力例を挙げます。

```
[Thu Jul 05 14:55:33 2018] [error] [client 192.168.0.100] ModSecurity: Access denied with code 403 (phase 2). Pattern match "\\\\bdocument\\\\b\\\\s*\\\\.\\\\s*\\\\bcookie\\\\b" at ARGS:login_email. [file "/etc/httpd/modsecurity.d/activated_rules/modsecurity_crs_41_xss_attacks.conf"] [line "107"] [id "958001"] [rev "2"] [msg "Cross-site Scripting (XSS) Attack"] [data "Matched Data: document.cookie found within ARGS:login_email: javascript:alert(document.cookie)"] [severity "CRITICAL"] [ver "OWASP_CRS/2.2.6"] [maturity "8"] [accuracy "8"] [tag "OWASP_CRS/WEB_ATTACK/XSS"] [tag "WASCTC/WASC-8"] [tag "WASCTC/WASC-22"] [tag "OWASP_TOP_10/A2"] [tag "OWASP_AppSensor/IE1"] [tag "PCI/6.5.1"] [hostname "example.com"] [uri "/frontparts/login_check.php"] [unique_id "Wz2y1cCoAa4AAEuTGnsAAAAC"]
```

適当に改行コードを入れます。

```shell-session
# grep 'ModSecurity: Access denied with code ' /var/log/httpd/ssl_error_log | tail -1 | sed 's/ \[/\n[/g'
[Thu Jul 05 14:55:33 2018]
[error]
[client 192.168.0.100] ModSecurity: Access denied with code 403 (phase 2). Pattern match "\\\\bdocument\\\\b\\\\s*\\\\.\\\\s*\\\\bcookie\\\\b" at ARGS:login_email.
[file "/etc/httpd/modsecurity.d/activated_rules/modsecurity_crs_41_xss_attacks.conf"]
[line "107"]
[id "958001"]
[rev "2"]
[msg "Cross-site Scripting (XSS) Attack"]
[data "Matched Data: document.cookie found within ARGS:login_email: javascript:alert(document.cookie)"]
[severity "CRITICAL"]
[ver "OWASP_CRS/2.2.6"]
[maturity "8"]
[accuracy "8"]
[tag "OWASP_CRS/WEB_ATTACK/XSS"]
[tag "WASCTC/WASC-8"]
[tag "WASCTC/WASC-22"]
[tag "OWASP_TOP_10/A2"]
[tag "OWASP_AppSensor/IE1"]
[tag "PCI/6.5.1"]
[hostname "example.com"]
[uri "/frontparts/login_check.php"]
[unique_id "Wz2y1cCoAa4AAEuTGnsAAAAC"]
```

POSTパラメータ `ARGS:login_email` に入力された値 `javascript:alert(document.cookie)` が検知された事が読み取れます。

一行が長いので要約します。

```shell-session
# tail -1 /var/log/httpd/ssl_error_log | perl -nle '/^(?:.*? ){3}(.*?) .*id "(.*?)".*msg "(.*?)"/ && print "$1 $2 $3"'
```

# WAFルールの無効化

利用者からの正当なリクエストを誤って検知してしまう偽陽性(false positive)が発生した場合、取り急ぎWAFルールを無効にして対応します。
正しい検知であり、アプリケーション側の問題であれば、修正して再度ルールを有効にします。

WAFに検知された場合、デフォルトでは 403 Forbidden となります。ベーシック認証やIP制限でも発生するので、WAFによるものかはログで確認します。

## カスタムルールファイル

ルールを完全にオフにする場合は SecRuleEngine を、個別にオフにする場合は SecRuleRemoveById を指定します。.htaccess以外の任意のコンテキストに記述できます。バーチャルホスト以外のコンテキストに記述する場合は、下記 `z_customrules.conf` を使用してください。SecRuleRemoveById は SecRule より後に評価される必要があるため、このファイルに記述するのが安全です。

原則、設定のカスタマイズは。下記ファイルを新規作成して記述してください。
- `/etc/httpd/modsecurity.d/activated_rules/modsecurity_crs_15_customrules.conf`
- `/etc/httpd/modsecurity.d/activated_rules/z_customrules.conf`

`modsecurity_crs_15_customrules.conf` は、定数初期化後かつルール定義前に読み込まれます。SecRuleより前に記述する必要があるSecDefaultActionやデフォルトルールより優先したいルールなどを記述します。
`z_customrules.conf` は、ルール定義後に読み込まれます。

## 検出オンリーモード

検知しても遮断せず、ログのみ記録します。

```/etc/httpd/conf.d/mod_security.conf
    SecRuleEngine DetectionOnly
```

## WAF機能を完全オフ

最終手段として機能を完全オフにするには、次のように設定します。

```/etc/httpd/conf.d/mod_security.conf
    SecRuleEngine Off
```

## 特定のURLでWAF機能を無効化

`/admin` 以下でWAFを無効化する設定は次の通りです。

```z_customrules.conf
<Directory /var/www/html/admin>
    SecRuleEngine Off
</Directory>
```

URLにファイルが対応していないならば、Locationを使用します。

```z_customrules.conf
<Location /admin>
    SecRuleEngine Off
</Location>
```

または、ルール個別に無効化する場合は、SecRuleRemoveById を記述します。引数のルールIDはスペース区切りで複数指定可能です。SecRuleRemoveById 自体を複数記述しても問題ありません。

```z_customrules.conf
<Directory /var/www/html/admin>
    SecRuleRemoveById 981317 950001 959073 981255 981245
    SecRuleRemoveById 950901 960024 981173 973300
</Directory>
```

## バーチャルホスト単位でWAF機能を無効化

バーチャルホストコンテキストに、特定のURLでWAF機能を無効化する設定を記述します。

```
<VirtualHost *:443>
    <IfModule mod_security2.c>
        SecRuleEngine Off
    </IfModule>
</VirtualHost>
```

`mod_security.conf`の管理外のコンテキストに記述する場合は、モジュールをアップロードしてもエラーにならないよう IfModule を指定しましょう。

# POSTサイズの調整

phpの`post_max_size`,`upload_max_filesize`に合わせて、WAFの`SecRequestBodyLimit`,`SecRequestBodyNoFilesLimit`を調整する必要がありそうです。
https://qiita.com/70_10/items/3cfa76710c2321fa0d63

# 参考情報

[本家wiki](https://github.com/SpiderLabs/ModSecurity/wiki)
[本家Reference Manual v2.x](https://github.com/SpiderLabs/ModSecurity/wiki/Reference-Manual-%28v2.x%29)
[本家Log Data Format](https://github.com/SpiderLabs/ModSecurity/wiki/ModSecurity-2-Data-Formats)