mattintosh note

どこかのエンジニアモドキの備忘録

EC-CUBE 4.1の.htaccessに関する考察

※本記事は2021 年 10 月 22 日に note に投稿した内容を移設したものです

所用で .htaccess 関連のドキュメントを作成することになったので EC-CUBE.htaccess を見て気になったことをツラツラと書きます。(issue 立てるのが苦手なので)

本記事に記載の内容は個人の見解です。コードの内容から意図が読み取れないため憶測を多分に含みます。また、記事をちょいちょい修正します。

2021年10月21日時点の .htaccess を読み解いていく

DirectoryIndex index.php index.html .ht

<Files ~ "/index.php">
   order deny,allow
   allow from all
</Files>

<FilesMatch "(?<!\.gif|\.png|\.jpg|\.jpeg|\.css|\.ico|\.js|\.svg|\.map)$">
   SetEnvIf Request_URI "/vendor/" deny_dir
   Order allow,deny
   Deny from env=deny_dir
   Allow from all
</FilesMatch>

<FilesMatch "^composer|^COPYING|^\.env|^\.maintenance|^Procfile|^app\.json|^gulpfile\.js|^package\.json|^package-lock\.json|web\.config|^Dockerfile|^\.editorconfig|\.(ini|lock|dist|git|sh|bak|swp|env|twig|yml|yaml|dockerignore|sample)$">
   order allow,deny
   deny from all
</FilesMatch>

<IfModule mod_setenvif.c>
 SetEnvIf Request_URI "\.(jpe?g|png)$" _image_request
</IfModule>

<IfModule mod_headers.c>
   # クリックジャッキング対策
   Header always set X-Frame-Options SAMEORIGIN

   # XSS対策
   Header set X-XSS-Protection "1; mode=block"
   Header set X-Content-Type-Options nosniff

   #webpにvaray
   Header append Vary Accept env=_image_request
</IfModule>

# デザインテンプレートを適用するため10Mで設定
<IfModule mod_php7.c>
   php_value upload_max_filesize 10M
</IfModule>

<IfModule mod_rewrite.c>
   #403 Forbidden対応方法
   #ページアクセスできない時シンボリックリンクが有効になっていない可能性あります、
   #オプションを追加してください
   #Options +FollowSymLinks +SymLinksIfOwnerMatch

   RewriteEngine On

   # Acceptヘッダがimage/webpを含む場合
   RewriteCond %{HTTP_ACCEPT} image/webp
   RewriteCond %{SCRIPT_FILENAME}.webp -f
   # *.jpg、*.pngファイルを*.webpファイルに内部的にルーティングする
   RewriteRule .(jpe?g|png)$ %{SCRIPT_FILENAME}.webp [T=image/webp]

   # Authorization ヘッダが取得できない環境への対応
   RewriteCond %{HTTP:Authorization} ^(.*)
   RewriteRule ^(.*) - [E=HTTP_AUTHORIZATION:%1]

   RewriteRule "^\.git" - [F]
   RewriteRule "^src/" - [F]
   RewriteRule "^app/" - [F]
   RewriteRule "^tests/" - [F]
   RewriteRule "^var/" - [F]
   RewriteRule "^vendor/" - [F]
   RewriteRule "^node_modules/" - [F]
   RewriteRule "^gulp/" - [F]
   RewriteRule "^codeception/" - [F]
   RewriteRule "^bin/" - [F]
   RewriteRule "^dockerbuild/" - [F]
   RewriteRule "^\.devcontainer/" - [F]
   RewriteRule "^zap/" - [F]

   RewriteCond %{REQUEST_FILENAME} !-f
   RewriteCond %{REQUEST_FILENAME} !^(.*)\.(gif|png|jpe?g|css|ico|js|svg|map)$ [NC]
   RewriteRule ^(.*)$ index.php [QSA,L]
</IfModule>
<IfModule mod_mime.c>
 # 拡張子.webpファイルはContent-Typeとしてimage/webpを返す
 AddType image/webp .webp
</IfModule>

# 管理画面へのBasic認証サンプル
#
# AuthType Basic
# AuthName "Please enter username and password"
# AuthUserFile /path/to/.htpasswd
# AuthGroupFile /dev/null
# require valid-user
#
# SetEnvIf Request_URI "^/admin" admin_path  # ^/adminは, 管理画面URLに応じて変更してください
# <RequireAll>
#     Require all granted
#     Require not env admin_path
# </RequireAll>

EC-CUBE は以前から mod_access_compat(Order Deny,Allow とかで制限するやつ)に依存したアクセス制限を使用しています。mod_access/mod_access_compat によるアクセス制限は Apache 2.4 では非推奨になりました。EC-CUBE 公式でシステム要件を「Apache 2.4.x」と公表しているのでこれに関してはもう mod_authz_core に移行していいと思っています。コード内ではちょくちょく Orderorder になっており、これでも動作的には問題ありませんが Visual Studio Codeシンタックスハイライトが効かないので OrderAllowDeny の方が望ましい記述になるかと思います。なお、本ドキュメントではキャピタライズを守って記述します。

EC-CUBE 3 系までは .htaccess 直下に Order Deny,Allow が記述されていましたが 4 系になってから削除されました。心機一転でルートの .htaccess を  https://github.com/EC-CUBE/ec-cube/commit/56b37b8d0b2d136f090a5becb8d4261f582753c5#diff-270939b4fba4be968ab78e23dc0207eb893744491980a25c99a27c809a82ddab で書き直したようです。Order ディレクティブのデフォルト値は Deny,Allow でデフォルトが許可になっていますが、将来的に変わらないとは言えないし、これをすべての人が覚えているわけでもないので明示的な記述はしておいた方がいいのではないかと思っています。

では上から簡単に見ていきます。まずは DirectoryIndex ディレクティブです。

DirectoryIndex index.php index.html .ht

これはディレクトリへのアクセスがあった場合に DirectoryIndex に指定した順番でファイルを探すもので .ht(設定上は ".ht*")というファイルは Apache のグローバルコンフィグでアクセス拒否となっているため「index.php と index.html が存在しない場合は .ht にアクセスしてその結果拒否になる」というものでよく見かけるセキュリティ向上設定のひとつです。

次に出てくるのが index.php へのアクセス制限ですが、前からおかしかったのですが最近更におかしくなりました。後述しますがこのセクションは設定した内容が後から上書きされるというかなり危うい状態になっています。

<Files ~ "/index.php">

Files ディレクティブの引数は filename、つまり「ファイル名」を取るため「パス」は取りません。恐らくこれは /index.php というパスにあるファイルではなく /index.php というスラッシュを含んだファイル名にマッチしているのではないかと思っています。Apache のソースまでは追ってないのですがとりあえずこの指定では index.php に効果はありません。

また、Files ディレクティブで ~ を使用した場合は引数を正規表現FilesMatch と同等)として扱うため正しく index.php を指定する記述としては下記のようになります。

<Files ~ "^index\.php$">

index.php に対して正規表現を使う必要もないでしょうから、

<Files "index.php">

でいいと思われるのですが昔からずっと ~ が付いていますし、ドットエスケープもされていない、先頭・末尾の指定も入っていないため filename の指定が曖昧で意図が汲み取れません。

この Files セクションは Order Deny,Allow となっているためデフォルトで許可となります。Order はマッチしなければ最後の引数がデフォルトになるため Allow from all は省略できますがわかりやすくするために明示的に書いているのだと思われます。(前述の通り .htaccess 直下には書いていませんが)

<Files "index.php">
    Order Deny,Allow
    Allow from all
</Files>


# Apache 2.4 (mod_authz_core)
<Files "index.php">
    Require all granted
</Files>

次に正規表現の否定後読みが使用された部分です。非常に問題のあるブロックです。

<FilesMatch "(?<!\.gif|\.png|\.jpg|\.jpeg|\.css|\.ico|\.js|\.svg|\.map)$">
   SetEnvIf Request_URI "/vendor/" deny_dir
   Order allow,deny
   Deny from env=deny_dir
   Allow from all
</FilesMatch>

恐らく「画像や CSS、JS ファイル以外(拡張子がコレで終わらないもの)」という条件を正規表現にしたものだと思われます。「画像や CSS、JS 以外」だと文章が書きづらいのでここでは「画像」または「画像以外」と表記します。

上記にマッチした画像以外のファイルのうち、リクエストに /vendor/ が含まれるものには deny_dir環境変数が設定されます。先頭の指定が無いのでルートディレクトリの /vendor だけでなくサブディレクトリの /html/vendor などもマッチします。これが意図した設定なのかはわかりません。(html/user_data/catalog/vendor とかディレクトリ作ったらアクセスできなくて困ることになる気がします)

このセクションは Order Allow,Deny ですのでデフォルトが拒否となり、Allow from all ですべて許可したあとに deny_dir 環境変数が設定されたリクエストのみを拒否しています。要約すると「/vendor/ ディレクトリ以外の画像ファイル以外をすべて許可」になるかと思います。

この記述によってサブディレクトリの .htaccess によるアクセス拒否設定が無効化されていたり、特定の Web サーバーで .htaccess が漏洩する可能性がある状態になっています。

また、気になるのが「先に記述されている index.php もこの『画像以外』にマッチするがどのような影響を受けるのか?」というところです。

Apache 公式の説明によると Directory ディレクティブ以外は上から順に処理されます。

以外は、それぞれのグループは設定ファイルに現れた順番に処理されます。http://httpd.apache.org/docs/2.4/ja/sections.html

つまり最初の Files ディレクティブで設定された index.php へのアクセス制限は次に現れる画像以外の FilesMatch ディレクティブで上書きされることになります。

EC-CUBE ではこの動作により過去に脆弱性が見つかりました。悪用を防ぐために敢えて詳細を公表していないのかもしれませんが、本来アクセスが許可されないファイルに対してのアクセスが有効になっていたとのことです。

現在もサブディレクトリ内の .htaccess によるアクセス制限が正しく効かない等の問題が残っています。

先に挙げたとおり「画像などの一部のファイル以外のアクセスをすべて許可する」ことによってそれより上部に記述されていた Deny from all がキャンセルされてしまったことが原因と思われます(修正内容も記述順を変更しただけのものでした)。

現在も根本的な問題は解決されていないように見られ、当該の FilesMatch ディレクティブよりも上に記述したものは上書きされてしまいます。試しに下記のように 2 つの Files ディレクティブを追加してみます。

<Files "1.php">
    Order Allow,Deny
    Deny from all
</Files>

<Files "1.png">
    Order Allow,Deny
    Deny from all
</FilesMatch>

<FilesMatch "(?<!\.gif|\.png|\.jpg|\.jpeg|\.css|\.ico|\.js|\.svg|\.map)$">
   SetEnvIf Request_URI "/vendor/" deny_dir
   Order allow,deny
   Deny from env=deny_dir
   Allow from all
</FilesMatch>

結果はご覧の通りで画像にマッチしない 1.php はアクセスできます。

$ curl -I localhost/1.php
HTTP/1.1 200 OK ★Deny from all にしているのにアクセスできてしまっている
Date: Thu, 21 Oct 2021 13:17:32 GMT
Server: Apache
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Frame-Options: SAMEORIGIN
Upgrade: h2,h2c
Connection: Upgrade
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
Content-Type: text/html; charset=UTF-8

$ curl -I localhost/1.png
HTTP/1.1 403 Forbidden
Date: Thu, 21 Oct 2021 13:17:32 GMT
Server: Apache
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Frame-Options: SAMEORIGIN
Content-Type: text/html; charset=iso-8859-1

もし当該の FilesMatch ディレクティブを使い続けるのであれば何らかの注意喚起が必要であると私は思っています。このセクションを使い続けるのが望ましいとは思っていません。

# /!\ これより上に記述した Allow, Deny は上書きされる可能性があります /!\
<FilesMatch "(?<!\.gif|\.png|\.jpg|\.jpeg|\.css|\.ico|\.js|\.svg|\.map)$">
   SetEnvIf Request_URI "/vendor/" deny_dir
   Order allow,deny
   Deny from env=deny_dir
   Allow from all
</FilesMatch>

※案の定「アクセス制限が効かなくなった」という issue が投げられていました。

github.com

Apache はプログラミングで言うところの if や not が使えなかったり扱いづらいこともあり、コードにすると複雑な条件を書かなければならないこともあります。(SetEnvIf だけで AND を実現するなど)

しかし、当該のセクションに関しては「特定の拡張子で終了するファイル以外において /vendor/ を除いてすべてアクセス許可する」という部分を読み替えた方が恐らくいいと思います。(意図が汲み取れないためこの条件を目的としているかどうかはわかりません)

そもそも Order ディレクティブはデフォルトが Deny,Allow ですから画像以外の拡張子をもつファイルに関しては最初からアクセスが許可されています。 恐らくやりたいことは「/vendor/ へのアクセスを拒否したい」ということだと思われます。(これについては後述します)

SetEnvIf Request_URI "/vendor/" is_access_vendor
Deny from env=is_access_vendor

「これだと /vendor/ 以下(厳密には以下ではありません)の画像や CSS なども拒否になるのではないか?」と疑問を持たれる方もいるかもしれませんが、そもそも vendor ディレクトリはこのセクションの下にある RewriteRule 部分で F フラグ、つまり 403 Forbidden にされているため画像だろうがなんだろうがアクセスは拒否されるためわざわざ「画像や CSS 以外の…」という指定をする必要があるのかどうか不明です。

RewriteRule "^vendor/" - [F]

/vendor/ 以下の画像や CSS に関してはアクセスを許可するというのであれば、下記のような RewriteCond が必要になると思われるのですが設定されていない理由はわかりません。

RewriteCond %{REQUEST_FILENAME} !\.(gif|jpe?g|png|css|ico|js|svg)$ [NC]
RewriteRule "^vendor/ - [F]

話が少し戻りますが mod_access_compat で「/vendor/ 全体に制限をかけつつ、/vendor/ 以下の画像について許可・拒否を設定する」のであれば上書きする性質を使って下記のように記述すればいいでしょう。Allow from env=is_access_vendorAllow from all でもかまいませんが、is_access_vendor 変数が設定されたものだけをキャンセルするような方が意図しない上書きが発生しないので安全だと思います。

# すべてのディレクトリ・ファイルはデフォルトで許可
Order Deny,Allow

# ルートディレクトリ直下の vendor ディレクトリを拒否に設定
SetEnvIf Request_URI "^/vendor($|/)" is_access_vendor
Deny from env=is_access_vendor

# 特定の拡張子のみ上記の拒否設定を許可で上書き
<FilesMatch "\.(?i:gif|jpe?g|png|css|ico|js|svg|map)$">
    Allow from env=is_access_vendor
</FilesMatch>

/vendor/1.png へのアクセスが行われた場合、

  1. /vendor/ へのアクセスなので Deny となる
  2. 拡張子が .png なので 1 で設定された Deny を Allow で上書きする

となります。/html/vendor/ に関してはルートディレクトリ直下の vendor ではないため影響を受けません。

なぜ vendor ディレクトリの保護を .htaccess で行う必要があるのか

EC-CUBE 4 系で外部に公開されてはならないディレクトリは vendor だけではなく、他にも appsrcvar といったディレクトリも存在します。しかし、上記のような特別な設定がされているのは vendor ディレクトリのみです。

vendor ディレクトリは少々特殊で開発環境には存在しないディレクトリです(GitHubEC-CUBE 公式リポジトリを見てください)。この「存在しない」という部分が恐らく .htaccess の複雑な設定をしなければならない要因になっていると思われます。

appsrc など、最初から存在しているディレクトリに関しては各ディレクトリ内に .htaccess が設置されており、各ディレクトリが自身でアクセスを拒否するようになっています。

app/.htaccess

order allow,deny
deny from all

vendor ディレクトリの場合、「composer install」コマンドによって初めてディレクトリが作成されるため予め .htaccess を配置することができません。そのためルートディレクトリの .htaccess で拒否するような設定をしているのだと思われます。

「予め vendor ディレクトリを作成しておいて .gitkeep と .htaccess を入れておけばいいのでは…」という疑問が湧いたのでやってみたところ特に問題は無さそうでしたが、もしかしたら何か理由があるのかもしれません。(いまから vendor.htaccess を配置したところですべての環境で保護される保証がない、等)

.htaccess の続き

話を vendor ディレクトリから戻して .htaccess を下っていきます。

私の嫌いな部分です。 「正規表現でまとめて記述しよう」という方針なのか、色々追加したら長くなってしまって今に至るのかはわかりませんが、どう見ても可読性が低すぎます。

<FilesMatch "^composer|^COPYING|^\.env|^\.maintenance|^Procfile|^app\.json|^gulpfile\.js|^package\.json|^package-lock\.json|web\.config|^Dockerfile|^\.editorconfig|\.(ini|lock|dist|git|sh|bak|swp|env|twig|yml|yaml|dockerignore|sample)$">
   order allow,deny
   deny from all
</FilesMatch>

分解してみるとこの正規表現には 2 つの用途が含まれていることがわかります。

# ファイルを指定するもの
^composer
^COPYING
^\.env
^\.maintenance
^Procfile
^app\.json
^gulpfile\.js
^package\.json
^package-lock\.json
web\.config
^Dockerfile
^\.editorconfig

# 拡張子を指定するもの
\.(ini|lock|dist|git|sh|bak|swp|env|twig|yml|yaml|dockerignore|sample)

1つは「ファイル名を指定」、もう一つは「拡張子を指定」です。web.config だけ先頭の記述が無いのですが入れ忘れなのかこれが正なのかはわかりませんが恐らく入れ忘れではないでしょうか。

EC-CUBE 4 系になってからディレクトリの構造が変わりブラックリスト形式では守るものが多すぎる印象です。 とりあえずこの正規表現を分けてもらいたいです。 格好良くて間違っているより格好悪くても正確である方がセキュリティ上は望ましいと思います。

分割しても長くはなりますが用途(ファイル名、拡張子)別で定義した方がわかりやすいとは思います。

<FilesMatch "^(COPYING|Dockerfile|Procfile|\.editorconfig|\.env|\.maintenance|app\.json|composer|gulpfile\.js|package-lock\.json|package\.json|web\.config)$">
   Order Allow,Deny
   Deny from all
</FilesMatch>

<FilesMatch "\.(?i:bak|dist|dockerignore|env|git|ini|lock|sample|sh|swp|twig|ya?ml)$">
   Order Allow,Deny
   Deny from all
</FilesMatch>

ちなみに拡張子の指定に .git が入っていますがこれは .git ディレクトリそのものに対してのみ有効で(/.git には効くけど /.git/ には効かない)、.git ディレクトリに含まれるサブディレクトリやファイル、それから .gitignore に対しては効果はありません。

表にすると下記のようになります。

        +------------------------------+
        | Request_URI                  |
+-------+------------------------------+
| Files | /.git | /.git/ | /.gitignore | 
+-------+-------+--------+-------------+
| .git  | Deny  | Allow  | Allow       |
+-------+-------+--------+-------------+
| .git* | Deny  | Allow  | Deny        |
+-------+-------+--------+-------------+

RewriteRule で先頭に .git がつくものに関しては一律拒否されるのですが、mod_rewrite が無効な場合には .git ディレクトリそのものに対してしか効果がないので .git.* とした方がまだマシなのではと思います。

さらに下にいきます。

クリックジャッキング対策と XSS 対策は以前からあるものですのでここでは説明を割愛します(この辺は MDN の説明を読むとわかりやすいと思います)。

最近になって WebP 用の設定が追加されました。

<IfModule mod_setenvif.c>
 SetEnvIf Request_URI "\.(jpe?g|png)$" _image_request
</IfModule>

<IfModule mod_headers.c>
   # クリックジャッキング対策
   Header always set X-Frame-Options SAMEORIGIN

   # XSS対策
   Header set X-XSS-Protection "1; mode=block"
   Header set X-Content-Type-Options nosniff

   #webpにvaray
   Header append Vary Accept env=_image_request
</IfModule>
:
中略
:
<IfModule mod_mime.c>
 # 拡張子.webpファイルはContent-Typeとしてimage/webpを返す
 AddType image/webp .webp
</IfModule>

コメント部分に typo が見られるのは置いといて mod_setenvif が使えないサーバ存在するのか疑問です。Apache のモジュールにはステータスというものがあり、mod_setenvif は Base であるため Apache の解説によれば、

"Base" のディレクティブは デフォルトでサーバに組み込まれている標準モジュールの中の一つでサポートされていて、わざわざ設定からモジュールを削除したときを除いて、 通常では使用可能であることを示します。

となっています。

さて、ここで設定される _image_request という変数ですが、Header append の部分でしか使用されていません。また、mod_headers が使えない場合は意味が無いので mod_headers の IfModule 内で定義した方が無駄が無いでしょうし。モジュールのすべての条件を満たした場合に WebP 対応の設定をするのであれば mod_headers、mod_mime の両方が必要になるので個別に指定せず、WebP 用の設定として入れ子で指定してもいい気がします。

RewriteRuleT=image/webp していて Add Type image/webp .webp と同じ効果があるので Add Type は要らない気もする)

<IfModule mod_headers.c>
    <IfModule mod_setenvif.c>
        <IfModule mod_mime.c>
            AddType image/webp .webp
            SetEnvIf Request_URI "\.(jpe?g|png)$" _image_request
            Header append Vary Accept env=_image_request
        </IfModule>
    </IfModule>
</IfModule>

残りは mod_rewrite の部分です。昔から使われている部分もありますが何で真ん中の部分にソートとかかけないんだろうな…と思っています。

Files ディレクティブではディレクトリ内部が指定できず、.htaccess 内では Directory ディレクティブが使えないことから RewriteRule で色々対応しているのだと思います。

なお、ここの Forbidden フラグを削除するとサブディレクトリのアクセス制限が正しくは動作せず、appvar といったディレクトリの内容が外部に漏れます。

新しく追加された WebP へのリダイレクト部分にドットのエスケープや大小文字無視などが設定されていないので考慮不足を感じますし、書くなら Authorization や Forbidden の下なのでは…と思ったり。最終的にアクセスは拒否されるのですが、現状は Forbidden より先に WebP への内部リダイレクトが発生しているのでややコストが無駄であると感じます。

<IfModule mod_rewrite.c>
   #403 Forbidden対応方法
   #ページアクセスできない時シンボリックリンクが有効になっていない可能性あります、
   #オプションを追加してください
   #Options +FollowSymLinks +SymLinksIfOwnerMatch

   RewriteEngine On

   # Acceptヘッダがimage/webpを含む場合
   RewriteCond %{HTTP_ACCEPT} image/webp
   RewriteCond %{SCRIPT_FILENAME}.webp -f
   # *.jpg、*.pngファイルを*.webpファイルに内部的にルーティングする
   RewriteRule .(jpe?g|png)$ %{SCRIPT_FILENAME}.webp [T=image/webp]

   # Authorization ヘッダが取得できない環境への対応
   RewriteCond %{HTTP:Authorization} ^(.*)
   RewriteRule ^(.*) - [E=HTTP_AUTHORIZATION:%1]

   RewriteRule "^\.git" - [F]
   RewriteRule "^src/" - [F]
   RewriteRule "^app/" - [F]
   RewriteRule "^tests/" - [F]
   RewriteRule "^var/" - [F]
   RewriteRule "^vendor/" - [F]
   RewriteRule "^node_modules/" - [F]
   RewriteRule "^gulp/" - [F]
   RewriteRule "^codeception/" - [F]
   RewriteRule "^bin/" - [F]
   RewriteRule "^dockerbuild/" - [F]
   RewriteRule "^\.devcontainer/" - [F]
   RewriteRule "^zap/" - [F]

   RewriteCond %{REQUEST_FILENAME} !-f
   RewriteCond %{REQUEST_FILENAME} !^(.*)\.(gif|png|jpe?g|css|ico|js|svg|map)$ [NC]
   RewriteRule ^(.*)$ index.php [QSA,L]
</IfModule>

まず、WebP へのリダイレクト部分のみ修正例を書いておきます。EC-CUBE 標準の機能で画像のアップロードなどをすれば拡張子が大文字になることはありませんが一応大小文字を考慮しておいた方がいい気がします。

  1. 正規表現で大小文字を無視する
RewriteRule \.(?i:jpe?g|png)$ %{SCRIPT_FILENAME}.webp [T=image/webp]
  1. NCフラグで大小文字を無視する
RewriteRule \.(jpe?g|png)$ %{SCRIPT_FILENAME}.webp [NC,T=image/webp]

Forbidden の部分ですがディレクトリそのものに対しての制限がかからない正規表現になっているので、例えば vendor ディレクトリであれば、

RewriteRule "^vendor/" - [F]

RewriteRule "^vendor($|/)" - [F,NC]

とした方がいいように思えます。

Apache の解説ではリダイレクトコードに 300-399 外を選択した場合は L が指定された場合と同様に書き換えが停止されるとあります。

if a status code is outside the redirect range (300-399) then the substitution string is dropped entirely, and rewriting is stopped as if the L were used.http://httpd.apache.org/docs/2.4/rewrite/flags.html#flag_r

なので、

RewriteRule "^vendor($|/)" - [R=404,NC]

と書けばファイルやディレクトリが存在しないように見せかけられますね。(これに関しては公式で設定すると「ファイルがあるのにファイルが無いと言われる」という問い合わせが殺到しそうなので個人でのアレンジの範疇になるかもしれません)

現状の記述だと /vendor へのアクセスは Forbidden にならず、ディレクトリへのアクセスであるため 301 Moved Permanently でスラッシュ付きの /vendor/ にリダイレクトされます。そして /vendor/ が条件にマッチして 403 となるため 301 部分が余計に発生する状態です。

RewriteRule の記述順としては、

  1. Authorization または Forbidden
  2. Webp
  3. index.php

でしょうか。

結局アクセス制限はどうするのがいいのか

個人的には行数が増えようとも複雑な正規表現などは使用せずわかりやすくミスが少なくメンテナンスがしやすい方がいいとは思っています。現状の EC-CUBE では複数の要件があるためこのような複雑な状態になっていると考えています。

  • mod_rewrite が使えない(動作しない)場合の考慮をしなければならない(RewriteRule だけでは完全な保護ができない)
  • Files ディレクティブではディレクトリ内部が指定できない
  • .htaccess では Directory ディレクティブが使えない
  • 予め .htaccess が配置できないディレクトリがある

Apache でのアクセス制限の方法は Directory/Files/Location ディレクティブや RedirectRewriteRule などの方法がありますが個人的には条件の要件を満たすには Redirect または RedirectMatch を使うのがいいのではないかと考えています。

Redirect または RedirectMatch は mod_alias モジュールでステータスも Base なので前述の通り使えない環境は恐らく少ないと思います。流石にステータスが Core な Directory/Files/Location には劣りますが DirectoryLocation と違って .htaccess でも使用できます。ステータスが Extension な mod_rewrite よりは使える環境も多いと思います。 

RedirectRedirectMatch の特徴としてステータスを指定できるというものがあります。アクセス拒否の場合、大抵は 403 Forbidden を返すものだと思いますが、個人的に 403 だとファイルが存在しているのがバレると思っているので下記の例では 404 にしています。

多少長くはなりますが今どき .gitignore でこれくらいの行数のファイルは見慣れているでしょうし、Git で管理する場合も1行に複雑な正規表現で並べるよりは差分が見やすいようになります。また、正規表現と違って記述ミスをしても各行で独立しているため他の項目への影響も少なくて済むでしょう。

プロジェクトで README.md を配置しているのをよく見かけますし、リリース前に削除するのを忘れていてヤバい情報が漏れてるのをたまに指摘することがあるので README.md も拒否設定に入れておいて方がいいでしょうね。(Git 管理してると削除すると差分になってインデックスを弄ったりしないといけないのでちょっとひと手間かかります)

Require all granted

# サブディレクトリを含めたディレクトリ・ファイル、またはパターン
RedirectMatch 404 /\.(?i:env)
RedirectMatch 404 /\.(?i:git)
RedirectMatch 404 /\.?(?i:composer)($|\.|/)

# ルートディレクトリ直下のディレクトリ本体と内包ディレクトリ・ファイル
RedirectMatch 404 ^/\.(?i:devcontainer)($|/)
RedirectMatch 404 ^/(?i:app)($|/)
RedirectMatch 404 ^/(?i:bin)($|/)
RedirectMatch 404 ^/(?i:codeception)($|/)
RedirectMatch 404 ^/(?i:dockerbuild)($|/)
RedirectMatch 404 ^/(?i:gulp)($|/)
RedirectMatch 404 ^/(?i:node_modules)($|/)
RedirectMatch 404 ^/(?i:src)($|/)
RedirectMatch 404 ^/(?i:tests)($|/)
RedirectMatch 404 ^/(?i:var)($|/)
RedirectMatch 404 ^/(?i:vendor)($|/)
RedirectMatch 404 ^/(?i:zap)($|/)

# ルートディレクトリ直下のファイル
RedirectMatch 404 ^/\.(?i:dockerignore)$
RedirectMatch 404 ^/\.(?i:editorconfig)$
RedirectMatch 404 ^/\.(?i:maintenance)$
RedirectMatch 404 ^/(?i:COPYING)$
RedirectMatch 404 ^/(?i:Dockerfile)$
RedirectMatch 404 ^/(?i:LICENSE\.txt)$
RedirectMatch 404 ^/(?i:Procfile)$
RedirectMatch 404 ^/(?i:README.md)$
RedirectMatch 404 ^/(?i:app\.json)$
RedirectMatch 404 ^/(?i:gulpfile\.js)$
RedirectMatch 404 ^/(?i:package-lock\.json)$
RedirectMatch 404 ^/(?i:package\.json)$
RedirectMatch 404 ^/(?i:web\.config)$

# 拡張子
RedirectMatch 404 (?i:\.bak)$
RedirectMatch 404 (?i:\.dist)$
RedirectMatch 404 (?i:\.ini)$
RedirectMatch 404 (?i:\.lock)$
RedirectMatch 404 (?i:\.sample)$
RedirectMatch 404 (?i:\.sh)$
RedirectMatch 404 (?i:\.swp)$
RedirectMatch 404 (?i:\.twig)$
RedirectMatch 404 (?i:\.ya?ml)$

ドットファイルに関しては公開するものはほとんど無いのですべて拒否設定でもいいかもしれません。

RedirectMatch 404 /\.

ドットファイルのうち「Let's Encrypt で使用する .well-known だけは許可したい」というのであれば下記のように設定する必要があるかもしれません。

RedirectMatch 404 /\.(?!well-known)

気になることとしては他のアクセス制限と併用した場合の動作です。

Apacheソースコードを細かく確認したわけではありませんが RedirectMatch は Directory/Files よりも Location に近い評価順と思われるため、下記のような順で設定したとしても(Files の後に評価されると思われるため)結果は 403 となります。

RedirectMatch 403 /1\.png$

<Files "1.png">
    Allow from all
</Files>

RewriteRule と組み合わせた場合は RewriteRule が先に評価されます(下記の結果は 403 です)。これは書き換えの後にリクエストが確定すると考えるとそうなのかなと思います。

RedirectMatch 404 /1\.png$

RewriteEngine On
RewriteCond %{REQUEST_FILENAME} ^1\.png$
RewriteRule \.png$ - [F]

index.php と html ディレクトリ以外は基本公開してなくていいのでそもそもブラックリスト形式をやめてしまうというのもひとつの手ではないかなと思います。​

特定の Web サーバーにおける .htaccess 漏洩の可能性について

新たに Xserver 等の特定の Web サーバーにおいて .htaccess 漏洩の可能性について書きました。

mattintosh-note.jp

おわりに

正直なところ公式の方針やポリシーがわからないため .htaccess から意図を汲み取ろうとしましたが限界がありました。

この .htaccess が今後どのような風になっていくのかはわかりませんが、ひとまず現状では .htaccess の上の方にアクセス拒否設定を書くのは危険だと私は見ています。

また、ここでは詳しく書きませんが環境によって情報が漏洩する問題があるようです。(流石にこれは公式に報告した方がよいのかもしれない)

mod_access_compat は卒業して欲しいですね…。