ピクシブ株式会社で開発基盤チームとして働いている @catatsuy です。
前編ではpixivを常時HTTPS化する前にやった前準備として、広告、画像といったリソースをHTTPSに切り替える際の手順を紹介しました。
pixivを常時HTTPS化するまでの道のり(前編) - pixiv inside
後編では実際にpixivのアプリケーション自体を常時HTTPS化していく手順を紹介します。
従来のHTTPS配信
pixivはPHPアプリケーションを実行するアプリケーションサーバー(Apache/mod_php)の前段にnginxを配置する構成になっています。以前からセキュリティ的に重要なページはHTTPSで提供しており、nginxでHTTPS終端処理を行っていました。HTTPSで応答する場合はアプリケーションサーバーにX-HTTPS
ヘッダーを付けてプロクシーしています。
具体的には以下のようなnginxの設定を使用していました。
HTTPSで提供しているページは一部だけなので、そのページだけをnginxからアプリケーションサーバーに流します。HTTPSで提供しないページはnginxでHTTPのページに302リダイレクトするようにしていました。
セキュリティ的に重要なページはアプリケーション側でX-HTTPS
ヘッダーの有無を確認して、無ければHTTPSにリダイレクトします。pixivではこの方法でこれまで一部のURLをHTTPSで提供してきました。すべてのURLに対するHTTPS通信をプロキシするようにすることで、常時HTTPSにすることができます。
常時HTTPSへの移行作業
常時HTTPSへの移行手順はざっくり以下のようになります。
- Ajaxで叩くAPIを先行してHTTPSに変更する
- HTTPとHTTPSの両方で見れるようにする
- HTTPへのGETリクエストだけをステータスコード302でHTTPSにリダイレクトする
- CSPでmixed contentsの対応漏れがないか探す
1つ1つ説明していきます。
Ajaxで叩くAPIを先行してHTTPSに変更する
ブラウザがリクエストするページに先駆けて、まずAjaxで叩くURLを先行してHTTPSに変更しました。AjaxリクエストではHTTPとHTTPSだけの差でもSame Origin制約に引っかかるのでCORS (Cross-Origin Resource Sharing)を使う必要があります。 弊社では以前からCORSを利用してきました。詳しくは以下、弊社でCORSを導入したときのエントリを参照してください。
今回気を付けなければならなかったのはpreflightリクエストです。CORSについて詳しくは以下のURLを参照してください。
HTTP access control (CORS) - HTTP | MDN
POSTリクエストでContent-Typeにapplication/json
を指定した場合など、CORSでは特定の条件を満たすと実際のリクエストを行う前にpreflightリクエストが行われます。
preflightリクエストはOPTIONSメソッドで行われます。その際Cookieは付与されません。そのためアプリケーション側でOPTIONSメソッドを考慮していない場合、認証が必要なAPIだとログインしていないと判断され、ログインページにリダイレクトするレスポンスを返すなどの想定外の挙動をします。
よって以下のような実装にしました。preflightリクエストでOPTIONSメソッドのリクエストが送られてきたら、アプリケーション側でOriginヘッダーを確認します。想定したOriginヘッダーならば、レスポンスにAccess-Control-Allow-Origin
やAccess-Control-Allow-Credentials
ヘッダーを適切に付けて、ステータスコード200を返します。これで正しく動作しました。
(HTTP access control (CORS) - HTTP | MDN by Mozilla Contributors is licensed under CC-BY-SA 2.5).
HTTPとHTTPSの両方で見れるようにする
次に、すべてのページを本番環境でHTTP、HTTPSの両方でリクエストできるように変更し、ページの動作を確認していきました。
事前の確認として、主要ページがHTTPS上で正しく動作していたら問題なしとしました。理由はpixivはURLが膨大でかつ機能も多いため、最初からすべての機能がHTTPS上で動くかを確認することは困難だからです。 主要な機能がHTTPS上で問題無く動いていたらHTTPS化を行い、後述のCSPでmixed contentsが発生していないか監視を行うという手順で順次HTTPSへ移行しました。
なお、HTTPS化は、ある程度独立して提供されている機能から順次に対応していきました。
たとえば、pixivの小説の機能はwww.pixiv.net/novel/
かtouch.pixiv.net/novel/
以下で機能が提供されています。このようにディレクトリが切られて、分割しやすいものから対応していくことで、順次HTTPS化を行えました。
HTTPへのGETリクエストだけをステータスコード302でHTTPSにリダイレクトする
HTTPSにリダイレクトする際に気を付けなければならないことがあります。POSTリクエストをステータスコード302でリダイレクトすると、リダイレクト先にGETリクエストを送ります。その際POSTリクエストで送ったデータは消えてしまいます。
一方で、ステータスコード307でリダイレクトすると多くのブラウザでPOSTメソッドのままPOSTしたデータをリダイレクト先に再送してくれます。しかし、ステータスコード307でPOSTリクエストをリダイレクトした場合、ブラウザ上で確認のダイアログが出ることがあります。ユーザーがPOSTで送ったデータを、他のURLに再送させるのはユーザーに不信感を与えるかもしれません。 以上の理由から、POSTリクエストはリダイレクトしないことが望ましいと考え、今回はPOSTリクエストならリダイレクトしないようにしました。
では、実際にリダイレクトする処理について説明していきます。今回の対応では、www.pixiv.net
とtouch.pixiv.net
でリダイレクトする方法を変えていたので、2つを分けて説明していきます。
touch.pixiv.netの場合
pixivは長い歴史があるサービスであり、ページごとに処理するPHPスクリプトが分かれている構成になっています。例えばプロフィールページのURLは https://www.pixiv.net/member.php?id=11 のようにPHPのファイル名にクエリストリングを渡しているURLです。このため、従来のページ構成では、すべてのページで統一した処理を行うような変更が困難になっていました。
現在pixivではこの問題を解決するためにソースコードの整理を進めており、URLルーティングの導入をすすめています。URLルーティングを用いると、すべてのURLが1つのPHPスクリプトを実行するようになるため、全ページに対して一括した処理を容易に行うことができるようになります。
pixivのURLルーティングについて、詳しくは以下のエントリを参照してください。
PHPで高速に動作するURLルーティングを自作してみた - pixiv inside
HTTPS化の作業を行っていた時点ではtouch.pixiv.net
はURLルーターの実装が入っており、アプリケーションの実装から全体を統一的に制御することができるようになっていました。
そこでURLルーターにHTTPSを許可するかどうかのルールを実装し、先程紹介したnginxの設定と同じ挙動をするよう変更しました。 その後nginxの設定を変更し、HTTPSからのリクエストをすべてアプリケーションに流すように修正し、HTTPSの制御をURLルーター側で行うように変更しました。これにより、アプリケーション側でHTTPSの細かい制御が可能になります。
事前に本番環境で確認ができるように、確認用に付与した特定のCookieを持っている場合に、常時HTTPSで見られるようにルーター側の実装を変更しました。
www.pixiv.netの場合
touch.pixiv.net
と違い、www.pixiv.net
はURLの種類が多く、ルーター化も行われていませんでした。そのためアプリケーション側でHTTPSを許可するかどうかを制御することが困難でした。
そこでアプリケーションのコードを変更せずに、アプリケーション全体の動きを変更するためにnginxで制御を行いました。
nginxで制御を行う場合、以下のデメリットがあります。
- 開発環境と本番環境の設定を合わせるのが難しい
- 挙動を確認するためには、nginx設定とアプリケーションコードの両方をみる必要があり、他の開発者がすべての挙動を追うことが難しくなる
- 特定のCookieを持っていたら動きを変えるといった複雑なロジックを書くにはnginx-luaなどを用いる必要がある
これらのデメリットを解決するのは難しいです。少しでもこれらのデメリットを抑えるために、HTTPとHTTPSが混在する期間を2日に抑えました。
touch.pixiv.net
では1週間混在期間がありましたが、そのために一部の機能が長期間動かないことがありました。
混在期間中は、もっとも動作を保証することが難しいため、混在期間を短く抑えることがおすすめです。
nginxの設定は以下のような設定を書きます。
nginxの設定をこのようにすることでPOSTリクエストのみ許可、GETリクエストはHTTPにリダイレクトする設定が書けます。ただしnginxのif
は非常に複雑なので慎重に使う必要があります。
CSPでmixed contentsの対応漏れがないか探す
CSPについて説明します。 CSPとは特定の種類の攻撃を検知したり、実際に影響を軽減させることができるセキュリティ上の仕組みです。指定したURIに対して、ポリシー違反の情報をJSONデータとしてまとめてPOSTすることができます。
詳しくは以下のURLを参照してください。
- mixed contents 対応を促進するCSPディレクティブ
- 混合コンテンツの防止 | Google Developers
- Content Security Policy (CSP) - HTTP | MDN
CSPはHTTPヘッダーを1行返すだけで有効にすることができ、最近のブラウザならばまず対応しています。Report-Onlyにすればユーザーに悪影響はないので安心して付けることができます。 指定方法によってmixed contentsの警告に使えるので、今回のようなHTTPで動いているサービスをHTTPSに移行する際にmixed contentsを見つける用途にも使用することができます。
CSPは以下のような記法で指定していきます。
Content-Security-Policy-Report-Only: default-src https: 'unsafe-inline' 'unsafe-eval'; img-src https: 'unsafe-inline' 'unsafe-eval' data: ; report-uri https://example.com/csp-report
report-uri
に指定するURLに制約はありません。POSTメソッドでJSONのデータが送られるだけなので、そのJSONを保存する適当なシステムがあれば容易に確認できるようになります。report-uri.ioという無料のサービスを使うこともできます。大量のリクエストを送ることはできませんが、気軽に始めたいならおすすめです。
ただCSPのレポートが増えたら通知する仕組みなど、工夫を行いたい場合は自社で保存するシステムを用意する必要があるでしょう。
CSPで送られるJSONは以下のようなJSONです。
document-uri
にポリシー違反が発生したURLが入ります。この際に気を付けなければならないのはURLのクエリパラメータは消える点です。pixivのURLでは前述の通り https://www.pixiv.net/member.php?id=11
のようにクエリパラメータを多く使用しています。
他社のサービスでも検索ページの検索クエリをクエリで渡しているサービスをよく見ます。その場合、特定の検索クエリでmixed contentsが発生していたとしても、https://www.pixiv.net/member.php
のように ?
以降のクエリが報告されないため、その検索クエリが分からない状況になるので注意が必要です。
また広告によってmixed contentsになるケースも検知したいところです。ユーザーにmixed contentsがどの程度発生しているか知りたいというのもありますが、広告の配信設定ミスなどで発生しているケースは早く見つける必要があります。ただし配信広告がiframeを作り、その中で呼ばれた広告タグがHTTPのリソースを呼ぶケースは検知できません。
touch.pixiv.net
の場合、ほとんどの広告はtouch.pixiv.net
上でscriptタグとして自社の広告配信サーバーを呼び出しています。このケースではtouch.pixiv.net
上でCSPのヘッダーを返せばmixed contentsを検知できます。
一方で、www.pixiv.net
の場合、ほとんどの広告はiframeで自社の広告配信サーバーのURLを呼び出して、その中で配信広告のタグを呼び出しています。このケースでは自社の広告配信サーバー上でCSPのヘッダーを返せばmixed contentsを検知できます。
ただし自社の広告配信サーバーでは必要な情報はすべてGETクエリに記述されていました。前述の通りCSPのレポートではGETクエリが消えます。そのため、「どの広告枠でmixed contentsが発生しているのか」など、基本的な情報が一切取れなかったため非常に困りました。
配信広告のほかにもブラウザの拡張機能(extension)が依存するアセットでもmixed contentsになります。このケースでもCSPのレポートが送られてきますが、サービスとしてできることは特になく、ノイズが大量に送られてきてしまいます。 つまりmixed contentsを完全に無くすためには拡張機能を作っている各社がすべてHTTPSを使用するようになる必要があるということです。拡張機能によるノイズは直ちに0にすることはできないと割り切り、必要な情報を探し出す必要があるでしょう。
CSPについて説明してきましたが、mixed contentsの対応漏れを発見できるのでとてもよい仕組みです。正直この仕組みがなければ、常時HTTPS化ができると思えませんでした。 不満がないわけではありませんが、有用な情報を取得できる数少ない方法なので確実に使っていくことになるでしょう。
発生した問題
以上で常時HTTPS移行した際の手順を紹介しました。ここから移行時に発生した問題を紹介します。
touch.pixiv.netで起こった問題
Chromeでは、HTTPS上でsetしたlocalStorageはHTTPからはread onlyになります。そのため一部のページがHTTPSに移行した際に、HTTPのページ上で一部機能が正しく動作しない問題がありました。 逆に、HTTPのページ上でsetしたものはHTTPSのページから操作できます。そのため一気にHTTPSに移行してしまえば、問題は解決します。
他にもreferrerを見ていた処理がHTTPでしか提供していなかったため、正しく動作しない問題が発生しました。これではHTTPSでアクセスしたときに、HTTPにリダイレクトをするとreferrerの情報が失われます。 そのため、referrerを見る処理はHTTPとHTTPS両方で使えるようにする必要があります。
referrerについてはReferrer-Policy
という仕様があるので紹介します。Referrer-Policy
のorigin-when-cross-origin
を使用するとHTTPSからHTTPのリソースを読み込んだり、ページを遷移した場合にオリジンのみ、つまりhttps://example.com
のような値だけがreferrerに入ります。
しかしCan I useによると新しい仕様であるorigin-when-cross-origin
に対応しているのは2017年6月現在ではChromeとFirefoxに限られます。IE/Edge/Safariは対応していません。これらのブラウザに対応していない値であるorigin-when-cross-origin
を指定するとreferrerの挙動が予想できなくなります。現実的に使用できるのは旧仕様のdefault
/never
/always
の3つに限られるでしょう。
他にも、DBに保存するデータが壊れる問題がありました。pixivの一部処理でpixivのイラストのURLなどが含まれていたら、イラストのIDなどの情報をJSONにしてDBに保存する処理がありました。そのURLの判定処理がHTTP固定になっていたため、HTTPSのURLを貼り付けたときに正しく動作していませんでした。そのためURLの判定ロジックを変更後、該当期間のデータで問題があるものの修正を行いました。
www.pixiv.netで起こった問題
www.pixiv.net
は一部のユーザーにだけ設定できる機能が多いため、mixed contentsの対応漏れがいくつか見つかりました。
その内の1つに外部APIをCORSで叩いて、画像URLを取得して表示する処理がありました。その画像URLがHTTPだったためmixed contentsになっていました。この処理はソースコード内をgrepしただけでは見つけることはできないため事前に発見できませんでした。
また前述の通り、pixivの場合GETクエリにはユーザーIDが入っています。CSPのレポートからどのユーザーで発生しているのか見つけることができず、該当処理を発見するのに苦労しました。
しばらく後に発生した問題
移行した直後は問題無かったにも関わらず、しばらく経った後に問題が発生したものがいくつかありました。ここでは2つ紹介します。
当初問題の無かった配信広告がHTTPの画像を読み込むようになったことです。アドネットワークの接続先やさらにその接続先などからHTTPのタグが読み込まれた場合、active mixed contentになるため、そもそも読み込まれません。しかしHTTPの画像は正常に読み込めるため、配信広告事業者側がログなどから気付くことは難しいです。 HTTPSで提供しているサービス上でHTTPの画像を読み込むとpassive mixed contentになり、サービスが安全ではないという表示になります。そのため不利益を被るのはサービスを提供している側、つまりpixivが不利益を被ります。 この問題はサービス提供者側がいち早く気付いて、配信広告事業者側に修正してもらう必要があるため、継続的にCSPのレポートを監視することが重要です。
またSSPやアドエクスチェンジでは、広告の在庫が引き当たらなかった時に呼び出す広告を指定できます。フィラーと呼ばれる設定です。 フィラーとしてHTTPのタグが指定されていましたが、フィラーの設定は在庫がある間はほとんど呼ばれないため、つい見逃しがちです。今回も、常時HTTPS化してしばらく後に顕在化しました。
後戻りできない変更
最後の仕上げとして後戻りできない変更をしていきます。後戻りできない変更は大きく以下の3つです。
- ステータスコード301でリダイレクトする
- HSTSを返す
- Cookieのsecure属性を付ける
1. ステータスコード301でリダイレクトする
今回、最初はステータスコード302のリダイレクトでHTTPSに移行しました。理由はステータスコード301を返してしまうとブラウザ側がキャッシュを持つため、サーバー側にアクセスすることなく移行先のURLにリダイレクトされてしまうからです。HTTPS移行後に致命的な問題が見つかりHTTPSでの配信を一時中断することになったとしても、一度リダイレクトをキャッシュしてしまった利用者がHTTP版のURLにアクセスすることがなくなり、正常にアクセスできなくなってしまう恐れがあります。そのため最初はステータスコード302でリダイレクトを行いました。
それならずっとステータスコード302でいいと考える方もいるかもしれません。しかしステータスコード301でリダイレクトを行わないとGoogle上で表示されるURLが変更されません。 ステータスコード302のリダイレクトはあくまでも一時的なリダイレクトと解釈されるからです。ステータスコード302のリダイレクトでもGoogleのbotはHTTPSの方にアクセスを始めますが、Google上の表示は変更されません。
HTTPSでも問題ないことが確認でき次第、ステータスコード301でリダイレクトするように変更しました。
2. HSTSを返す
ステータスコード301でHTTPからHTTPSにリダイレクトするようにしたら、HSTS(HTTP Strict Transport Security)を必ず返すことをおすすめします。
ステータスコード301でリダイレクトをするようにしても、ユーザーがHTTPのURLにリクエストを送ることがあるからです。それだとHTTPSでアクセスできるにも関わらず、セキュリティレベルが下がってしまいます。 HSTSを返せばブラウザはキャッシュを持ち、そのキャッシュがある間はそのドメインに対して、HTTPSのリクエストしか送らなくなります。HTTPのURLが指定されていたとしても、内部的にHTTPSへリダイレクトを行います。HTTPでリクエストを送りません。
前述の通り、HSTSもユーザーがキャッシュを持つ設定です。一度返してしまうと基本的には戻せません。
3. Cookieのsecure属性を付ける
ここまでやれば、Cookieのsecure属性をつけるべきです。Cookieのsecure属性を付与するとユーザーがHTTPのURLに対してリクエストを送った際にCookieを付与しなくなります。 特にユーザーの認証情報を保持するCookieのsecure属性はつけるべきです。常時HTTPS化が終了した当時は、pixivのCookieを触っているサービスがpixivのみではなかったため、周辺サービスの対応が必要でした。現在では周辺サービスの対応が完了したため、Cookieのsecure属性をつけています。
最後に
ユーザーが安全にサービスを利用できるために、常時HTTPS化は既に必須と言って過言ではありません。pixivのように大きなサービスの場合、様々な人や企業を巻き込まなければ常時HTTPS化は進められません。しかし根気強く進めていけば、不可能なことではありません。
常時HTTPS化により、今後サービス内でWebRTCなどの新しい技術を使って新しい体験をユーザーに提供したり、HTTP2を使って高速な通信を提供したりすることが可能となります。これらについても今後試していきたいと考えています。
今回、前後編にわたって常時HTTPS化を行う際の作業方針、気をつけるべき点、発生した問題を紹介しました。この記事が、今後常時HTTPS化を行う方のお役に立てれば幸いです。
エンジニアを募集中です
ピクシブ株式会社では巨大なシステムを根本から変えていくことに興味があるエンジニアを大募集中です!