正規表現の否定の先読みで admin-ajax.php を除外しつつ /wp-admin/ 以下のアクセスを拒否する

WordPress と Apache (HTTPD Server) を使う場合、管理領域 `/wp-admin/` 以下への悪意あるアクセスから WordPress を守る Apache のアクセス制御の設定を書くことがありますが、 `/wp-admin/admin-ajax.php` は一般ユーザにもアクセスさせる必要があります。

拒否設定と例外的な許可設定の2個のブロックを並べて書く方法が主流ですが、正規表現の否定の先読みで書くと設定に必要なブロックが1個と簡潔になり便利です。

# WordPress の管理領域 `/wp-admin/` 以下を守る

まずはよく見かける `Satisfy Any` による方法を書いてみます。

## `Satisfy Any` を使って拒否設定を上書きする設定を書く方法

WordPress ではブルートフォースアタックなど悪意あるアクセスを遮断するため、管理ツール領域 `/wp-admin/` 以下をウェブサーバレベルで一般のアクセスを拒否することが多く行われます。

```apache
<LocationMatch "^/wp-admin/">
        AuthType Digest
        AuthName "interest_ae"
        AuthDigestDomain /
        AuthUserFile /var/www/auth/post.digest
        Require valid-user
</LocationMatch>
```

上記では DIGEST 認証を使用していますが、BASIC 認証や IP アドレスによるアクセス制御をすることもあるでしょう。

注意しないといけないのは、 **`/wp-admin/` 以下であっても `/wp-admin/admin-ajax.php` は一般ユーザがアクセス可能でなくてはならない** 点でしょう。

上述の `/wp-admin/` 一般アクセス拒否と両立するため、改めて `/wp-admin/admin-ajax.php` は許可をするという設定を書く解説はよく見かけます。

```apache
<LocationMatch "^/wp-admin/">
        AuthType Digest
        AuthName "interest_ae"
        AuthDigestDomain /
        AuthUserFile /var/www/auth/post.digest
        Require valid-user
</LocationMatch>

<Location "/wp-admin/admin-ajax.php">
        Satisfy Any
        Order allow,deny
        Allow from all
</Files>
```

[Satisfy ディレクティブ](http://httpd.apache.org/docs/2.4/mod/mod_access_compat.html#satisfy) は、[Allow ディレクティブ](http://httpd.apache.org/docs/2.4/mod/mod_access_compat.html#allow)と [Require ディレクティブ](http://httpd.apache.org/docs/2.4/mod/mod_authz_core.html#require) が両方適用される際、どちらかの許可を満たせば許可されるのか (Any)、どちらの許可を満たして初めて許可されるのか (All) を指示するためのディレクティブです。なお Satisfy のデフォルトは All なので、 `Satisfy Any` は明示的に書く必要があります。

[Order ディレクティブ](http://httpd.apache.org/docs/2.4/mod/mod_access_compat.html#order) や [Allow ディレクティブ](http://httpd.apache.org/docs/2.4/mod/mod_access_compat.html#allow) は若干のややこしさがありますが、上記の記載だと「全許可」となります。

日本語で読むと

- `/wp-admin/` 以下へのアクセスを許可（認可）するには DIGEST 認証をクリアする必要がある
- ただし `/wp-admin/admin-ajax.php` へのアクセスは、前述の `/wp-admin/` への認証に成功するか「全許可」のどちらかを満たす（ `Satisfy Any` ）なら許可される
    - 認証の可否はともかく、「全許可」の方は無条件で適用されるので、結果的に `/wp-admin/admin-ajax.php` は全許可として誰もがアクセス可能となる

といった感じです。

慣れている人ならいいのですが、Satisfy Order Allow あたりは Apache 独特の話なので（これに類似する認証認可の機構は他のソフトウェアにもあるでしょうが）パッと見て分かりづらい印象を覚える人も多いと思います。私も Apache を使って長く経ちますが、未だにパッと見てすぐに読めないことがあります。

## 正規表現の否定の先読みで `/wp-admin/admin-ajax.php` を拒否設定から除外する

「 `/wp-admin/` で始まる文字列だけど `/wp-admin/admin-ajax.php` にはマッチしない」という正規表現が書ければ、LocationMatch ブロックディレクティブ1つで意図した拒否ができそうです。

実はそういう正規表現を書くことができます。これを実現するのが正規表現の **否定の先読み** です。

実際に見てみましょう。

```apache
<LocationMatch "^/wp-admin/(?!admin-ajax\.php)">
        AuthType Digest
        AuthName "interest_ae"
        AuthDigestDomain /
        AuthUserFile /var/www/auth/post.digest
        Require valid-user
</LocationMatch>
```

正規表現の `(?!☆)` は **否定の先読み (negative lookahead)** という書き方で、その位置から先（書字方向が向かう先、日本語を含む大部分の言語では右側）にその正規表現 ☆ が無いことをマッチ条件の一つにするという書き方です。

例えば以下のような例があります

- `田中(?!さん|さま|様)` … 有効な敬称が伴っていない「田中」にマッチする（推敲時に敬称漏れチェックをする場合など）
- `京都(?!市|府)` … 京都市や京都府ではない「京都」にマッチする

なので正規表現 `^/wp-admin/(?!admin-ajax\.php)` は

- `/wp-admin/` から始まる文字列にはマッチ条件の一つを満たすが
    - しかし `/wp-admin/admin-ajax.php` と続く文字列にだけはマッチしない
    - それ以外の `/wp-admin/` から始まる文字列にはマッチする

という正規表現となります。

# 否定の先読みってどの環境で使えるの？

否定の先読み、初学者向け書籍で正規表現が取り上げられていたとしても解説されないことが多い、いわば中級者向け機能です。

世間で広く使われる正規表現の構文は、大きく分けて「基本正規表現」「拡張正規表現」「Perl互換正規表現」の3種類あります。 **否定の先読みが使える正規表現の構文は、このうち「Perl互換正規表現」と呼ばれるもの** です。

| 名前 | 英語名 | 英語略称 | 使われる場所 |
|:-:|:-:|:-:|:-:|
| 基本正規表現 | basic regular expression | BRE  | sed や grep の標準 |
| 拡張正規表現 | extended regular expression | ERE  | sed や grep の拡張モード、find など |
| **Perl互換正規表現** | Perl compatible regular expression | PCRE | Perl、PHP(preg系関数)、Ruby をはじめとしたプログラミング言語、Apache 設定ファイル |

このうち Apache は、設定ファイルの正規表現を評価するために、pcre という文字通り PCRE を解釈する正規表現ライブラリを使用しています 。よって否定の先読みが使えます。

突然 Apache の設定ファイルに否定の先読みの正規表現を書くと、組織によっては今後保守する人が読めない懸念があるかもしれません。設定ファイルにこの記事の URL をコメントとして書いておくと、未来に保守する人が助けられるかもしれません。

正規表現は可読性を損なうとして嫌う人もいますが、要所で使うことで今回のように短い正規表現が全体の簡素化に貢献することもあります。正規表現のちょっと進んだ使い方に興味が湧いた方は、巻末の参考文献などからぜひ調べてみて下さい。

# 参考

- [正規表現 - Wikipedia](https://ja.wikipedia.org/wiki/%E6%AD%A3%E8%A6%8F%E8%A1%A8%E7%8F%BE)
- [第58回　正規表現の勘所―わかりづらい記法の覚え方，先読みや後読みの実践（3）：Perl Hackers Hub｜gihyo.jp … 技術評論社](https://gihyo.jp/dev/serial/01/perl-hackers-hub/005803)
- [O'Reilly Japan - 詳説 正規表現 第3版](https://www.oreilly.co.jp/books/9784873113593/) （最初に手を取る書籍としては難しいかも）