職業プログラマの休日出勤

職業プログラマによる日曜自宅プログラミングや思考実験の成果たち。リアル休日出勤が発生すると更新が滞りがちになる。記事の内容は個人の意見であり、所属している(いた)組織の意見ではない。

CakePHPをLBの裏側で使うときのリダイレクトを正しくする

先月、新規システムをCakePHPで作ったときに表題のことを久し振りにやったのですが、色々と忘れていたのでここにメモしておきます。

問題

CakePHPのControllerにて以下のようなリダイレクトを返すとき、リダイレクト先のURLをHTTPSつまり https://example.com/ で返して欲しいのに平文HTTPつまり http://example.com/ で返されてしまいます。
その結果、LBでは平文HTTPを受け付けていないので、Webブラウザはサイトにアクセスすることができなくなります。

return $this->redirect(['action'=>'index']);
前提条件

以下のような、古典的なシステム構成での話です。

  • Load Balancer(負荷分散装置、代表例はAWSのALB。以下、単にLBと言う。)では、外部からHTTPSTCP/443)のリクエストのみを受け付けて、平文のHTTP(TCP/80)のリクエストを受け付けない。
  • PHPが稼働するアプリケーションサーバでは、Nginx+php-fpm や Apache 等が稼働しており、LBからの平文のHTTPのリクエストを受け付ける。

なぜ、この問題が発生するのか?

一言で言うと、CakePHPの立場では「平文のHTTPでリクエストが来た」ように見えるから、です。
コードで言うと、アプリの config/bootstrap.php にある fullBaseUrl 自動生成ロジックが、リクエストを平文HTTPであると認識してしまっていました。

// 出典 : https://github.com/cakephp/app/blob/472c9e21e51909dda05336a7072eb5394334eb6a/config/bootstrap.php#L145-L148
$s = null;
if (env('HTTPS')) {
    $s = 's';
}

変数 $s の値がsであるとき、作られるfullBaseUrlは https:// となりますが、前述の前提条件のもとで特段何も設定をしない場合は env('HTTPS') の値はnullとなるので、平文扱いとなります。

解決策その1:環境変数を仕込む

上記のコードから、環境変数 HTTPS を設定できれば解決することがわかります。
このメモを書き留めるにあたって、この環境変数の挙動の流れについて(Apache + mod_php 限定ですが)詳しく書かれた記事を見つけたのでリンク貼っておきます。
qiita.com

解決策その2:bootstrap.php をいじる

多くのLB、例えばAWSのALBであれば、外部からHTTPSで来たリクエストは、背後のアプリケーションサーバに送る際には、リクエストヘッダ X-Forwarded-Proto付加するようになっています。これを利用すると、前述の bootstrap.php を以下のように書き換えることで、LB配下で稼働する場合にHTTPS認定をうまくやってくれるようになります。

$s = null;
if (env('HTTPS') || env('HTTP_X_FORWARDED_PROTO') === 'https') {
    $s = 's';
}

解決策その3:fullBaseUrl を仕込む

例えば、CLICakePHP流に言うと Console Command)からメールを配信する機能があって、そのメールには当該WebアプリのURLを記載するようにする要件がある場合、これがもっとも望ましい解決策でしょう。何故ならばHTTP/HTTPS云々以前に、ホスト名等の設定が必要であるからです。

筆者はどの解決策を採用したのか?

管理の手間等を踏まえると、解決策その2が一番簡単だと判断して採用していました。2.x系の頃の記憶はもう吹き飛びましたが、3.x系の頃からこうしてきました。(そして、久しぶりにやろうとして忘れていたので、メモとしてこの記事を書き始めました)。
…ところが最近になって、先月作ったアプリケーションにCLIからのメール送信の機能を追加することになりそうなので、そのアプリでは解決策その3に乗り換えようとしているところです。

なぜ解決策その2はデフォルトでは入っていないのか?

アプリケーションのフレームワークとしては、解決策その2の対応はデフォルトで入っていた方が良いのでは?と思われる方はそれなりにおられるのではないかと思いますが、デフォルトで入れておくと、ちょっとした問題があります。
github.com
HTTPSと平文HTTPとを両方使用している環境であり、かつ、LBを使っていない環境において、proxy鯖や中間者攻撃などによりリクエストヘッダがいじられることで、平文HTTPのURLを作るべきところで意図せずHTTPSのURLを作らせることができる、という攻撃が成立します。この攻撃によって何か被害が発生する構成というのは、現代では極めて稀(または筆者のセキュリティ能力や想像力の不足😇)だろうとは思いますが、上記の英語のコメントのように、フレームワークを作る側としては当時(2013年)はちょっと勇気が出なかったようです。

ちなみに、2022年3月19日(日本時間)に、アプリのスケルトン(テンプレート)が改修されて、少し楽になりました。
github.com
この修正によって、bootstrap.phpのロジックは↓のようになりました。

    /*
     * When using proxies or load balancers, SSL/TLS connections might
     * get terminated before reaching the server. If you trust the proxy,
     * you can enable `$trustProxy` to rely on the `X-Forwarded-Proto`
     * header to determine whether to generate URLs using `https`.
     *
     * See also https://book.cakephp.org/4/en/controllers/request-response.html#trusting-proxy-headers
     */
    $trustProxy = false;

    $s = null;
    if (env('HTTPS') || ($trustProxy && env('HTTP_X_FORWARDED_PROTO') === 'https')) {
        $s = 's';
    }

なので、3月19日以降にアプリケーションを新規に開発し始めた場合、つまり当日以降に composer create-project --prefer-dist cakephp/app:4.* hogehoge を実行したプロジェクトは、この trustProxytrue に変えるだけで、解決策その2を適用できることになります。

できればこの trustProxy の値は、ServerRequestのtrustProxyと連動して欲しいとは思うものの、まぁ開発が面倒なのはわかります。。

動作確認時のバージョン情報等