mattintosh note

数年ぶりにカメラを買ったのでフィギュアを撮る練習をしています

EC-CUBE 4系のディレクトリ構造を3系の構造にしてセキュリティを高める

なぜこんなことをしようと思ったのか

EC-CUBE 4 系から index.php がルートディレクトリに配置されるようになり、3 系と比べるとファイルやディレクトリのセキュリティレベルが低下しているように感じます。

たとえば Git を使ってソースを管理している場合、ルートディレクトリに README.md を置いたりするけどアクセス拒否されていない…などですね。(README.md は公式の GitHub でも配置されていて .htaccess で除外設定がされていないのですがどういう扱いなんでしょうか)

実際、これまで関わったパートナーのエンジニアさんでもプロジェクト情報を README.md に書いていてめちゃくちゃ外部に漏れてるっていうことがありました。

4 系ではレンタルサーバ向けに URL に html をつけないで動くようにする方に寄せたことで現在の構造になっているのかもしれませんが、個人的には 3 系のディレクトリ構造の方が安全(心配事が少ない)と思っています。そこで、4 系を 3 系のディレクトリ構造で動かせるか試してみました。

やってみようと思ったきっかけは Symfony の公式ガイドでデフォルトディレクトリ設定を上書きできるというものを見かけたからです。(これを見ると index.php はそもそも Symfony では public 以下、つまり公開領域下に置いているようです)

How to Override Symfony's default Directory Structure

https://symfony.com/doc/4.4/configuration/override_dir_structure.html

今回は下記のように index.phphtml 以下に移動して EC-CUBE が動くようにしてきたいと思います。

ec-cube/
|
+-- .env
|
+-- app/
|
+-- html/
|   |
|   +-- index.php
|   |
|   +-- template/
|   |
|   +-- upload/
|   |
|   +-- user_data/
|
+-- vendor/
   |
   +-- autoload.php

index.php

まずはルートディレクトリにある index.phphtml/index.php に移動します。(個人的には /var/www/html と被るので html じゃなくて public にしてもらいたいなぁ、といつも思います)

移動したら index.php 内の autoload.php を呼び出す部分と .env を参照する部分を書き換えていきます。メンテナンス用の .maintenancemaintenance.php の階層も変わるので同様に書き換えます。

diff --git a/html/index.php b/html/index.php
index 103b4e6..843c3a0 100644
--- a/html/index.php
+++ b/html/index.php
@@ -10,7 +10,7 @@ if (version_compare(PHP_VERSION, '7.1.3') < 0) {
    die('Your PHP installation is too old. EC-CUBE requires at least PHP 7.1.3. See the <a href="http://www.ec-cube.net/product/system.php" target="_blank">system requirements</a> page for more information.');
}

-$autoload = __DIR__.'/vendor/autoload.php';
+$autoload = __DIR__.'/../vendor/autoload.php';

if (!file_exists($autoload) && !is_readable($autoload)) {
    die('Composer is not installed.');
@@ -23,14 +23,14 @@ if (!isset($_SERVER['APP_ENV'])) {
        throw new \RuntimeException('APP_ENV environment variable is not defined. You need to define environment variables for configuration or add "symfony/dotenv" as a Composer dependency to load variables from a .env file.');
    }

-   if (file_exists(__DIR__.'/.env')) {
-       (new Dotenv(__DIR__))->overload();
+   if (file_exists(__DIR__.'/../.env')) {
+       (new Dotenv(__DIR__.'/..', '.env'))->overload();

        if (strpos(getenv('DATABASE_URL'), 'sqlite') !== false && !extension_loaded('pdo_sqlite')) {
-           (new Dotenv(__DIR__, '.env.install'))->overload();
+           (new Dotenv(__DIR__.'/..', '.env.install'))->overload();
        }
    } else {
-       (new Dotenv(__DIR__, '.env.install'))->overload();
+       (new Dotenv(__DIR__.'/..', '.env.install'))->overload();
    }
}

@@ -55,7 +55,7 @@ if ($trustedHosts) {

$request = Request::createFromGlobals();

-$maintenanceFile = env('ECCUBE_MAINTENANCE_FILE_PATH', __DIR__.'/.maintenance');
+$maintenanceFile = env('ECCUBE_MAINTENANCE_FILE_PATH', __DIR__.'/../.maintenance');

if (file_exists($maintenanceFile)) {
    $pathInfo = \rawurldecode($request->getPathInfo());
@@ -67,7 +67,7 @@ if (file_exists($maintenanceFile)) {
        $baseUrl = \htmlspecialchars(\rawurldecode($request->getBaseUrl()), ENT_QUOTES);

        header('HTTP/1.1 503 Service Temporarily Unavailable');
-       require __DIR__.'/maintenance.php';
+       require __DIR__.'/../maintenance.php';
        return;
    }
}

.htaccess と html/.htaccess

ルートディレクトリはドキュメントルートではなくなりますので 3 系の .htaccess と同じように全体的にアクセスを拒否するようにするだけです。

/.htaccess

Require all denied

mod_access_compat で書きたい場合は下記のようにします。(非推奨)

/.htaccess

Order Allow,Deny

html 以下は 3 系と同じになり .env などの重要なファイルは一つ上の階層にあるのでごちゃごちゃしたアクセス制限を書く必要はなくなります。

https://github.com/EC-CUBE/ec-cube3/blob/3.0/html/.htaccess にある 3 系の html/.htaccess と同じものでいいかと。4.1 から PHP ファイルの実行抑制が入りましたがそのままでは index.php まで動かなくなるので Files index.phpindex.php のみ実行できるようにします。(サブディレクトリの index.php どうするねんってなるかもしれませんが)

Apache モジュール版はテストしてないです

/html/.htaccess

Require all granted

<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
</IfModule>

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

   RewriteEngine On

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

   # さくらのレンタルサーバでサイトへのアクセスをSSL経由に制限する場合の対応
   # RewriteCond %{HTTP:x-sakura-forwarded-for} !^$
   # RewriteRule ^(.*) - [E=HTTPS:on]

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

RemoveType .php
AddType text/plain .php
<FilesMatch "\.(php|phar)$">
   Require all denied
   SetHandler None
</FilesMatch>

<Files "index.php">
   Require all granted
   <IfModule !mod_php5.c>
       <IfModule !mod_php7.c>
           SetHandler "proxy:unix:/run/php-fpm/www.sock|fcgi://localhost"
       </IfModule>
   </IfModule>
   <IfModule mod_php7.c>
       SetHandler application/x-httpd-php
   </IfModule>
</Files>

EC-CUBE 公式では .php のみ対象にしているのですが PHP パッケージの conf では下記のように .phar にもハンドラーをセットしているので両方無効にしないと .pharPHP スクリプトを実行できてしまいます。

<FilesMatch \.(php|phar)$>
    SetHandler application/x-httpd-php
</FilesMatch>

composer.json

Symfony のガイドにしたがって公開領域の場所を変更します。extra.public-dir の値を . から html に変更します。

/composer.json

diff --git a/composer.json b/composer.json
index cc932e0..dcbe63a 100644
--- a/composer.json
+++ b/composer.json
@@ -178,7 +178,7 @@
        "bin-dir": "bin",
        "src-dir": "src/Eccube",
        "config-dir": "app/config/eccube",
-       "public-dir": "."
+       "public-dir": "html"
    },
    "config": {
        "platform": {

ここまでの変更で EC-CUBE 自体は動くはずですが画像などが呼び出せていないので次の項目で修正していきます。

framework.yaml

Twig 内で使われている asset() 関数が使用するパスが /html からになっているので html を削除していきます。

/app/config/eccube/packages/framework.yaml

diff --git a/app/config/eccube/packages/framework.yaml b/app/config/eccube/packages/framework.yaml
index 6145711..91ab734 100644
--- a/app/config/eccube/packages/framework.yaml
+++ b/app/config/eccube/packages/framework.yaml
@@ -23,20 +23,20 @@ framework:
    php_errors:
        log: true
    assets:
-     base_path: '/html/template/%eccube.theme%'
+     base_path: '/template/%eccube.theme%'
      packages:
        admin:
-         base_path: '/html/template/admin'
+         base_path: '/template/admin'
        save_image:
-         base_path: '/html/upload/save_image'
+         base_path: '/upload/save_image'
        plugin:
-         base_path: '/html/plugin'
+         base_path: '/plugin'
        install:
-         base_path: '/html/template/install'
+         base_path: '/template/install'
        temp_image:
-         base_path: '/html/upload/temp_image'
+         base_path: '/upload/temp_image'
        user_data:
-         base_path: '/html/user_data'
+         base_path: '/user_data'
        # json_manifest_path: '%kernel.project_dir%/public/build/manifest.json'
    cache:
        # this value is used as part of the "namespace" generated for the cache item keys

これで .env などの重要なファイルが外部に公開される危険性はかなり下がります。ルートディレクトリが外部に公開されていないので Git 関連のファイルを配置したりプロジェクトで共有したい README.md を置いても外部には公開されません。(.git などの Git 関連のファイルは現状 .htaccess でアクセス拒否されるようになっていますが万全とは言えません)

Symfony のドキュメントでは下記のようなヒントも書かれています。これを読むとやはり index.php は公開領域ディレクトリに置かれているのが望ましいのではないかと感じます。

Some shared hosts have a public_html/ web directory root. Renaming your web directory from public/ to public_html/ is one way to make your Symfony project work on your shared host. Another way is to deploy your application to a directory outside of your web root, delete your public_html/ directory, and then replace it with a symbolic link to the public/ dir in your project.

https://doc4.ec-cube.net/spec_directory-structure には 3 系のディレクトリ構成で運用する方法の記載が無いのですができれば公式でカスタマイズする方法を公開してくれればありがたいですね…。