2022年9月9日金曜日

PHPカンファレンス2022にてSPAセキュリティ超入門の話をします

今年もPHPカンファレンスにてトークさせていただくことになりまして、以下のようなお話をいたします。

日時:9月25日(日) 14:40〜15:40
場所:大田区産業プラザPiO  および YouTube
費用:無料
講演タイトル:SPAセキュリティ超入門
申し込み: connpass
アジェンダ:

SPA(Single Page Application)の普及が一層進んでおり、従来型のMPAを知らないウェブ開発者も生まれつつあるようです。SPA対応のフレームワークでは基本的な脆弱性については対策機能が用意されていますが、それにも関わらず、脆弱性診断等で基本的な脆弱性が指摘されるケースはむしろ増えつつあります。
本セッションでは、LaravelとReactで開発したアプリケーションをモデルとして、SQLインジェクション、クロスサイトスクリプティング、認可制御不備等の脆弱性の実例を紹介しながら、現実的な対策について紹介します。LaravelやReact以外のフレームワーク利用者にも役立つ説明を心がけます。


実は昨年も「SPAセキュリティ入門」というテーマでトークをしておりまして、今年はもう少し易しめということで「超入門」としました。

昨年の講演のスライドおよび動画は以下から参照ください。

昨年は主にSPAにまつわるセッション管理に焦点をあてて、JWTによるセッション管理は是か非か、JWT等トークン類の保存場所はCookieかlocalStorageかという基本的な考え方(しばしば論争になる)のお話をしました。

今年はもう少し現実的なテーマとして、アジェンダにもあるように、LaravelによるサーバーサイドAPI、Reactによるフロントエンドという構成のSPAを題材として、以下のような「よくある」脆弱性がいかにして混入するか、およびその対策について説明します。

  • 認可制御不備
  • SQLインジェクション
  • クロスサイトスクリプティング(XSS)

これら脆弱性は、LaravelおよびReactを普通に使っていれば混入しないはずのものです。しかし脆弱性診断等ではこれら脆弱性はしばしば目にします。普通に作れば混入しないはずの脆弱性がなぜ混入するのか。私はその原因を追い続けていましたが、どうも従来型のアプリケーション(マルチページアプリケーション=MPA)では常識だった知識が、SPAでは伝承されていないのではないかと考えるに至りました。この要因ファクターXについて紹介するとともに、なぜ今基本的な脆弱性が多いのかを説明しようと思います。

なお、前述のようにファクターXは従来の常識ですので、「なぁんだ、そんなことか」となること請け負いですが、「そんなこと」レベルのことが問題になっているように考えています。

それでは、よろしくお願いいたします。

2022年7月4日月曜日

メタップスペイメント不正アクセス事件の第三者報告書から攻撃の模様を読み解く

株式会社メタップスペイメントの運営する決済代行システムから約288万件のクレジットカード情報が漏洩した不正アクセス事件について、第三者委員会の報告書および経済産業省の行政処分(改善命令)があいついで公開されました。

第三者委員会調査報告書(公表版)
クレジットカード番号等取扱業者に対する行政処分を行いました (METI/経済産業省)

本稿では、主に第三者委員会の調査報告書(以下「報告書」と表記)をベースとして、この事件の攻撃の様子を説明します。

システムの概要

報告書にはシステム構成図やネットワーク構成図は記載されていないため、報告書の内容から推測によりシステムの構成を以下のように仮定しました。

図中のサーバー名は報告書の記載に従っています。以下、概要を説明します。

サーバ名概要
A社アプリ一般社団法人A 会員向け申込みフォーム
経産省改善命令では、「同社とコンビニ決済に係る契約を締結していた加盟店にサービスを提供するために開発、運用していたアプリケーション(以下「加盟店向けアプリ」という。)」が該当すると思われる
K管理画面社内用決済管理画面。A社アプリのサーバーと同居していた(*1)
決済サーバ決済機能を提供するサーバー。攻撃対象ではなかった可能性があるが機能としては存在するので記載した
データベース各システム共用のデータベース
報告書内にデータベースの種類は明記されていないが、用語集にMySQLの説明があるので、MySQLを使用していると推測
復号化サーバ報告書に登場する。暗号化されたクレジットカード情報を復号を提供するAPIと思われるが、不正アクセスに関する記載はない(*2)

注 *1 : 報告書中には、A社アプリとK管理画面が同居していた旨は明記されていませんが、時系列表には、事故後の対応として「管理画面サーバから確認用サイト、A社アプリの分離(2022年1月7日)」を実施した旨が記載されているので、元々これらは同一サーバに同居していたと考えられます。

注 *2 : 報告書には「Web2系に暗号化されたカード情報に係る復号化サーバが配置されていた。Web2系には、決済システムも配置されており、そこで暗号化されたカード情報も管理されていた。」と記載されています。報告書中では「サーバー」、「サイト」、「画面」、「機能」の使い分けが明確でないため断言は難しいのですが、「Web2系」がサーバーを意味すると解釈すると、決済サーバと復号化サーバは同一マシンに同居していた可能性があります。


時系列の流れ

第三者委員会の報告書に加えて、メタップス社の2月28日づけリリースも合わせて時系列の流れをまとめました。

日時出来事
2021年8月2日メタップスペイメントの決済データセンターに対する不正アクセスが開始。K管理画面へのXSS攻撃か?
2021年8月31日K管理画面に対する不正ログイン始まる
2021年10月14日A社アプリにSQLインジェクション攻撃始まる
2021年10月15日A社アプリに、SQLインジェクションで得たパスワードによる不正ログイン
2021年10月19日
~10月27日
A社アプリのSQLインジェクションにより2万5千件のカードデータ窃取
2021年10月25日
~12月14日
K管理画面不正ログインによりカード番号全桁の検索が実行される(2万件程度)
2021年10月25日メタップスペイメントがA社アプリのメンテナンス中にSQLインジェクション攻撃を発見
2021年10月27日メタップスペイメントがA社アプリのSQLインジェクション対策を完了
2021年11月11日A社アプリのアップロード機能よりバックドアが設置され、攻撃開始。最終的にカード情報データベースの全データが窃取されたとみられる(460,395件 + 2,415,750件)
2021年12月14日アクワイアラ E 社からイベントペイで不正利用懸念の連絡
2021年12月16日イベントペイでクレジットカード決済を停止
2022年1月6日K管理画面の管理用サイトにBasic認証を追加
2022年1月7日K管理画面サーバから確認用サイト、A社アプリの分離、A社用DBとペイメントDBの分離
2022年1月8日A社アプリのサーバの分離を実施
2022年1月24日メタップスペイメントがバックドアプログラムの存在を確認、削除
2022年1月24日メタップスペイメントが不正アクセス被害を公表
2022年1月28日管理用サイトのクロスサイト・スクリプティングに対応、Basic認証の追加
2022年2月28日メタップスペイメントが不正アクセスによる情報流出を公表

攻撃の流れの詳細については以下で説明します。


1. K管理画面 のアカウント情報の窃取、不正ログイン

報告書によると、以下の順で攻撃が行われました。

  1. K管理画面のXSS脆弱性を悪用し、当管理画面のID・パスワードを窃取(報告書には時期の記載がないが、2022年2月28日づけメタップス社のリリース記載の2021年8月2日の不正アクセスが該当するか?)
  2. X氏アカウントによる不正ログイン(2021年8月31日~)
  3. メタップス社内にてX氏アカウントのパスワードを変更(2021年9月末から2021年10月初旬までの間)これ以降X氏アカウントの不正ログインなし
  4. Y氏アカウントにて不正ログイン(2021年10月6日以降)

攻撃の模様を下図に示します。

報告書によると、XSS攻撃を許してしまった理由は下記のとおりです。

  • 自社で実施した脆弱性診断ではXSS脆弱性が検出されていたが、高危険度の脆弱性があるとPCI DSSの審査に通らないため、報告書を改ざんして脆弱性自体をなかったことにした
  • WAFが導入されていなかった(PCI DSSではコードレビューまたはWAFのいずれかの導入を求められているので、WAF導入は必須ではない)

XSS攻撃の詳細

K管理画面は社員向けのシステムであるので、画面の詳細は外部の攻撃者にはわからないはずです。報告書ではXSS攻撃の詳細は発表されていませんが、管理画面を狙うXSS攻撃自体は最近よく見かけるもので、おそらく問い合わせ画面のように外部から入力できるフォームからJavaScriptによる攻撃コードを注入したのではないかと予想されます。その種の攻撃の例としては、Water Pamolaが知られています。Water Pamola型のXSS攻撃の例については以下の動画を参照ください。


なぜパスワードが窃取できたか?

次にXSS攻撃でK管理画面のパスワードが窃取できた理由を考察します。こちら報告書には書かれていないので、「ありそうな経路」を列挙するにとどめます。

  1. 管理機能としてパスワードを平文で表示する箇所があった
  2. ログイン状態で自分自身のパスワードを変更した(パスワード再入力なし)
  3. 他の管理者のパスワードを変更する機能を悪用した(パスワード再入力なし)
  4. ログイン中のユーザあるいは他の管理者のパスワードをリセットする機能を悪用した
  5. 偽のタイムアウトを引き起こして「IDとパスワードを入力してください」という画面を表示した(12:56追記)

1は通常はあり得ないのですが、この事件では「あり得ないことが幾つも起こっている」のでないと断言することもできません。ありそうな経路は2または3ですが、そうすると、本来の管理者がログインできなくなります。その段階で気づきそうなものですが、報告書には当初不正ログインに使われていたX氏のパスワードが変更されたとあるものの変更した理由は明記されていない(ちなみに時期も明確ではない)ため、「X氏は攻撃者にパスワードを変更されたことに気づかないままパスワードをリセットした」可能性も考えられると思います。

社内用決済管理画面はインターネットからアクセスできた

K管理画面に不正ログインされたということは、社内用決済管理画面にインターネットからアクセスできたことを意味します。これも奇異な状態ですが、報告書の時系列表には、2022年1月6日に「管理用サイトにBasic認証を追加」とあるので、それまではインターネットから自由に当該システムにアクセスできたものと推測されます。


2. SQLインジェクション攻撃によるカード情報とパスワードの窃取

報告書には以下のように記載されています。以下引用です。

攻撃者は、2021年10月14日から2021年10月27日に渡り、A社アプリに対するSQLインジェクション攻撃により、暗号化されたカード番号、マスクされたカード番号及びA社管理画面の管理者アカウント情報をそれぞれ不正取得した。

この攻撃の様子を下図に示します。




A社アプリは元々他のクラウドに設置されていたものですが、2018年に東京データセンターに移設され、その際、データベースのテーブルはカード情報データベースと同じスキーマに設置されていたとのことです(データベーススキーマの未分離)。このため、A社アプリに対するSQLインジェクション攻撃により、カード情報まで窃取されたことになります。

SQLインジェクションによるパスワード窃取と不正ログイン

また、A社管理画面の管理者アカウントの窃取と不正ログインについては以下のような時系列となっています。

2021年10月15日 05:09SQLインジェクションによりID・パスワードの窃取
2021年10月15日 05:12不正ログイン

IDとパスワードの窃取から、わずか3分後に不正ログインされていることから、パスワードは平文で保存されていた可能性が高いと推測されます。仮にハッシュ値等で保存されていたとしても3分間で平文パスワードを復元されたのでは意味がありません。この不正ログインが次項の「バックドアプログラムの設置」につながります。

SQLインジェクション脆弱性が残置された理由

SQLインジェクション攻撃を許してしまった理由は、前述のWAFの未設置の他、A社管理画面の開発経緯が原因だったようです。以下報告書からの引用です。

上記ソースコード・レビューに関する社内規程が作成されたのは、2012年10月であるところ、A社アプリが委託先であるB社によって開発されたのは、2007年頃であるため、当時は、社内的にも同アプリに対してSQLインジェクション攻撃への対策としてソースコード・レビューを実施することは必須とされていなかった。また、MPにおいては、以前より「決済システム以外は脆弱性対策をする必要がない」との認識があったため、同規定の作成時において、当時フロントシステムにあったA社アプリが見直し的にソースコード・レビューの対象となることもなかった。

A社アプリが東京データセンターに移設された後も、同様の認識により、ソースコード・レビューや脆弱性診断の対象にはなっていなかったようです。


3. バックドアプログラムの設置と攻撃

こちらも報告書から引用します。

攻撃者は、2021年10月15日、A社管理画面に一度不正アクセスしているが、更に2021年11月11日、A社管理画面に不正アクセスを行い、A社アプリの管理機能の一つであるファイルアップロード機能を悪用し、バックドアプログラムを設置した。

そして、不正ファイル経由で、データベース内から、暗号化されたカード情報を含む当時格納されていた全ての情報を不正取得したと考えられる。

この様子を下図に示します。




ファイルアップロード機能によるバックドアプログラム(おそらくWebShell)を設置するのは定番の攻撃方法です。報告書には「東京DCがA社アプリと同じJavaで構築されているため」という記述があるのため、A社アプリはJavaで構築されていることがわかります。なので、バックドアプログラムは、JSP記述のWebShellと推測されます。

また、「不正ファイル」という用語はバックドアプログラムとは別のものとして記載されているようです。不正ファイルの中身は判然としませんが、WebShellから起動されたリバースシェルあるいはデータベース(MySQL)アクセス用のツールではないかと思われます。

バックドア設置はSQLインジェクション対策後に行われた

A社アプリのSQLインジェクション脆弱性対策は2021年10月27日に完了していますが、A社アプリのパスワードを悪用してのバックドア設置は2021年11月11日に実行されています。SQLインジェクション対応の一環として、A社アプリのパスワード変更を実施しておけば、バックドア設置はできなかったはずです。

復号鍵の窃取

また、報告書には記載がありませんが、経産省の改善命令には復号鍵について以下の記載があります。

当該クレジットカード決済システム内のデータベースに保存していた暗号化されたクレジットカード番号(マスキングされたクレジットカード番号を含む。)、有効期限、セキュリティコード及びこれらを復号化するための復号鍵が窃取され

復号鍵が窃取されたと明記されていますが、復号鍵がデータベースに保存されていたとは考えにくく、また「復号化サーバ」が存在する以上は、復号鍵はそこにあると考えるのが自然です。

報告書には復号鍵の漏洩ルートに関する記載はないようですが、時系列表には以下の記載があります。

2022年1月28日 復号APIのディレクトリトラバーサルに対応。

 このため、復号APIのディレクトリトラバーサル脆弱性が悪用されて復号鍵が窃取された可能性があります。

任意形式のファイルをアップロード可能だった

管理画面にファイルアップロード機能があること自体はよくあることですが、この場合、任意ファイル名で任意の内容のファイルをアップロードできると、簡単にWebShell等を設定されてしまうので通常はファイル名やファイルの中身に制限をつけます。しかし、報告書によると、以下のような記載があり、

アップロード機能の設定の不備 により、想定以外のファイルをアップロード可能だった為、バックドアプログラムを設置されていた。

拡張子.jspのファイル等をアップロードできていたことがわかります。

ファイル改ざん検知の不備

バックドアプログラムの設置は一種の「改ざん」にあたるため、ファイル改ざん検知システムが導入されていれば、バックドア設置を早期に発見できた可能性があります。メタップスペイメント社はPCI DSS 3.2.1完全準拠をうたっていましたし、PCI DSSではファイル改ざん検知システムの導入を要求しています。A社アプリはPCI DSSの対象外だったようですが、このアプリケーションにも改ざん検知システム(ファイル整合性監視ツール)を導入して監視しておけば、早期にバックドアの設置を検知して対応できた可能性があります。また、この記事の冒頭で推測しているようにA社アプリとK管理画面が同一サーバーに同居していたとするならば、PCI DSSの観点からも、ファイル改ざん検知の対象にすべきであったと考えます。

もっとも当社の監視体制については、報告書に以下の記載もあるため、改ざん検知システムを導入していただけでは有効に機能しなかった可能性が高いです。

従業員のヒアリング結果によれば、実際はセキュリティアラートにする検証ができる人材が不足しており、また、必要な範囲でセキュリティアラートを発信するようにするためのシステム面での調整(チューニング)が出来ていないことも相まって、MPの従業員は、セキュリティアラートが発信されても、特段気にして監視していなかったとのことであり、十分な検証がなされていなかったことが認められた。

事故後の対応として、2022年1月29日に「改ざん検知設定を修正」とあるため、改ざん検知システム自体は元々導入されていたようです。


4. 管理画面 への再度の不正アクセス 及び カード番号照会開始

報告書には以下のように書かれさています(2021年10月25日から同年12月14 日)。

上記SQLインジェクション攻撃及びバックドアプログラムにより、既にデータベースからマスクされたカード番号を不正取得しており、K管理画面上で不正取得したマスクされたカード番号を検索照会することによって、平文のフル桁のカード番号を閲覧することができたと考えられる。

また時系列表には以下のように記載されています。

海外IPアドレスによりUserID「Y氏」を利用してカード番号の検索が行われ、不正取得した閲覧用パスワード入力後、平文のフル桁のカード番号の検索結果が表示された。
なお、この間、平文のフル桁のカード番号を確認可能なURLに対し、約2万回不審な連続アクセスが確認された。

この攻撃の様子を下図に示します。

クレジットカード窃取は3経路存在した

ここまで説明した攻撃手法から、クレジットカード情報を攻撃者が入手した経路は以下の3経路があることになります。

  1. A社アプリ経由: SQLインジェクション攻撃によりマスク化カード情報と暗号化カード情報を入手(2万5千件、2021年10月27日まで)
  2. バックドア経由: バックドアにより暗号化カード情報と復号鍵を入手(288万件、2022年1月18日まで)
  3. K管理画面経由: K管理画面のもつクレジットカード番号検索機能の悪用による平文全桁カード情報取得(2万件、2021年12月14日まで)

3のK管理画面経由での平文カード情報取得は2021年12月14日で終わっています。攻撃者がなぜ経路3をこの時期にやめたかという理由を推測すると、この時期に復号鍵が入手できたため、経路3を悪用する理由がなくなったから、というのが私の推測です。

事故対応の過程で、当初メタップスペイメント社は上記1および3の経路のみを把握していたようですが、以下のように、1および3以外の経路があることに気づきます。

MPは、2022年1月21日に受領したK社12件のログの社内検証結果として、判明している原因(K管理画面に対する不正アクセスやSQLインジェクション攻撃)以外でクレジットカード情報が窃取されていると判断した(1月8日までの対策のみでは情報漏えいを防ぎきれていないことを認識した。)。

その後の調査にて、2022年1月24日にバックドアプログラムの発見、削除に至ります。


まとめ

メタップスペイメントのクレジットカード情報漏えい事件の概要を第三者委員会の報告書を元に説明しました。決済代行事業社からクレジットカード情報が大量に漏洩するという衝撃的な事件でありましたが、漏洩に至る経緯も驚くべきもので、第三者委員会報告書はセキュリティ関係者にとって非常に学びの多い資料だと思います。

侵入の発端となった脆弱性は、クロスサイトスクリプティングおよびSQLインジェクションという非常に基本的なものでありますが、仮にそれらの脆弱性があっても、他の保険的な対策や侵入発見後の対処が適切であれば、被害を最小限に留められたはずという点でも学びが多く、機会があれば別の記事で紹介したいと思います。

クロスサイトスクリプティングやSQLインジェクションなどの基本的な脆弱性や、当事件で悪用されたファイルアップロード機能やアカウント管理に関する対策方法については下記の書籍にて説明しています。

2022年5月16日月曜日

DNSリバインディング(DNS Rebinding)対策総まとめ

サマリ

DNSリバインディングが最近注目されている。Google Chromeは最近になってローカルネットワークへのアクセス制限機能を追加しており、その目的の一つがDNSリバインディング対策になっている。Googleが提供するWiFiルータGoogle Nest WiFiはデフォルトでDNSリバインディング対策機能が有効になっている。 DNSリバインディング対策は、攻撃対象アプリケーションで行うべきものであるが、ブラウザ、PROXYサーバー、リゾルバ等でも保護機能が組み込まれている。本稿ではそれら対策機能の状況と対策の考え方について説明する。

DNSリバインディング(DNS Rebinding)とは

DNSリバインディングはDNS問い合わせの時間差を利用した攻撃です。DNSのTTL(キャッシュ有効期間)を極めて短くした上で、1回目と2回目の問い合わせ結果を変えることにより、IPアドレスのチェック等を回避する攻撃です。下図は、DNSリバインディング時のDNS問い合わせの様子です。




DNSリバインディングの主な脅威

DNSリバインディングによる脅威は、DNS問い合わせがあるところ全てにありえますが、典型的な脅威として下記があります。

  • 外部からアクセスできない対象へのブラウザ経由での攻撃
  • SSRF攻撃のチェック回避

ブラウザ経由での攻撃

ブラウザ経由での攻撃の概要図を示します。

上図は、攻撃者の誘導により、被害者が罠のページ(http://trap.example.org/)を閲覧しているところです。罠ページが閲覧された瞬間にDNSのAレコード(IPv4アドレス)を変更し、罠ページは10秒後にXMLHttpRequestにより http://trap.example.org/secret.html をアクセスします。この時点ではIPアドレスは変更されているためイントラネット内のサーバーにアクセスします。この攻撃により、外部からは直接アクセスできないサーバーにアクセスできます。

このように、DNSリバインディングは、イントラネット内のサーバーの他、ルーターやファイアウォール、IoT機器など、利用者のパソコン自身など、「外部からはアクセスできないが内部ネットワークからはアクセスできる」機器やサーバーが主な攻撃対象です。IPアドレスのみでアクセス制御されているサイトも攻撃対象になります。

攻撃対象の例として、Ruby on Railsの開発環境がありました。Railsの開発支援機能web-consoleはDNSリバインディングに脆弱で、任意のコードを外部から実行できる問題が指摘されていました。

DNS rebinding attacks protection in Rails 6

このため、バージョン6以降で開発版でのDNSリバインディング対策が入るようになりました)。具体的にはHostヘッダのチェックであり、これはDNSリバインディングの定石的な対策方法の一つです。

この機能は開発環境のみでプロダクション環境では無効になります。 これは、Web-consoleのようなデバッグ支援環境でリモートコード実行の脆弱性が入りやすい(現実にあった)ので、開発環境だとDNSリバインディングの脅威があるという想定だと思います。

【参考】


SSRF攻撃との組み合わせ

SSRF攻撃の際に使われるDNSリバインディングについては以下の記事をお読みください。

EC2上でDNS RebindingによるSSRF攻撃可能性を検証した | 徳丸浩の日記

ここでは概要を説明します。以下のようなスクリプトがEC2上で動いているとします。外部からURLを取得して、その内容を表示するものです。

$url = $_GET['url']; $urlinfo = parse_url($url); $host = $urlinfo['host']; // URLからホスト名を取り出し $ip = gethostbyname($host); // 接続先IPアドレスを取得 if ($ip == "169.254.169.254") { // AWSのIMDSチェック die("Invalid host $host"); // IPアドレスが169.254.169.254ならエラー } $ch = curl_init($url); // URLからコンテンツ取得、表示

上記のスクリプトに、IPアドレス 169.254.169.254 のチェックをしていますが、これはEC2のIMDS (Instance Metadata Service)という機能を悪用されることを防ぐためです。IMDSは下記のように、169.254.169.254という仮想的なエンドポイントにアクセスすると、参照元インスタンスの設定を返します。以下は、EC2インスタンスに付与されたIAMクレデンシャルを参照する様子です。

[ec2-user@web ~]$ curl http://169.254.169.254/latest/meta-data/iam/security-credentials/XXXX { "Code" : "Success", "LastUpdated" : "2022-05-08T04:17:09Z", "Type" : "AWS-HMAC", "AccessKeyId" : "ASIAR6Mxxxxxxxxxxxxx", "SecretAccessKey" : "Wrt7en1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "Token" : "IQoJb3JpZ2luX2VjEAQaDxxxxxxxxxxxxxxxx...", "Expiration" : "2022-05-08T10:52:42Z" }

先に示したスクリプトは複数の抜け道があるのですが、その一つがDNSリバインディングによる攻撃です。以下は、DNSサーバーにdigコマンドにより連続してアクセスしたところ、2回目にIMDSのIPアドレスが返っている様子です。

[ec2-user@web ~]$ dig example.net +short ; dig example.net +short 203.0.113.2 169.254.169.254 [ec2-user@web ~]$

これにより、IPアドレスチェックの際は無害な内容を返し、本番アクセスの際はIMDSのIPアドレスということになって、SSRF攻撃によりIMDSの内容を盗むことができます。

【参考】


DNSリバインディングの対策

下図はDNSリバインディングの対策ができる場所を示しています。


以下、図中の数字の順に説明します。

① 対象サイト側の対策

DNSリバインディングの根本対策は、攻撃対象のサイトや機器側で行われるべきです。DNSリバインディングの対策は以下のいずれかにより対策できます。

  • ホスト名のチェック、あるいはダミーのデフォルトバーチャルホスト
  • 認証機能の実装(安全なパスワードを設定すること)

以下はNginxでダミーのバーチャルホストを設定している例です。本番サイトはexample.jpですが、それ以外のホスト名のリクエストはすべてダミーのコンテンツが返されることになります。ダミー側には機密情報や機能などはないので、DNSリバインディング攻撃を「受け流す」ことができます。

# ダミーのバーチャルホスト(Hostヘッダがexample.jp以外はこちらが設定される) server { listen 80 default_server; server_name '_'; root /var/www/dummy; # 無害なコンテンツが返される } # 本番サイトのバーチャルホスト server { listen 80; server_name example.jp; …

あるいは、認証機能の実装でも対策になります。DNSリバインディング攻撃は利用者のセッションを乗っ取れるものではないからです。IoT機器やルーターなど初期パスワードが設定されているものは、必ず初期パスワードを変更する必要があります。

【参考】

SSRF対策のパイパスについては、SSRF自体の対策の中で考える必要があります。詳しくは以下のコンテンツを参照ください。

② ブラウザ側対策

DNS Pinning

主要ブラウザでは、DNSリバインディング対策としてDNS Pinning(DNSピニング)が実装されています。DNS Pinningとは、DNSのTTLが短い場合でも、DNSの参照結果を一定時間保持することです。下図は主要ブラウザのDNS Pinningの期間です。

Google Chrome Firefox Safari IE
1分程度 1秒~70秒 15秒~30秒 無期限?

DNSリバインディング対策という観点からはDNS Pinningの期間は長いほど安全ですが、DNSリバインディング以外の正当な状況でサーバーのIPアドレスが変化する場合もあるので、過度にDNS Pinningの期間が長いと副作用もあると考えられます。DNS Pinningの期間は時代とともに変化しており、最近はウェブサイトの本来のIPアドレスが短期間で変化するため、DNS Pinningの期間は短めにされる傾向があります。

Google Chromeで提唱されている対策

Google ChromeではDNS Pinning以外に「ローカルネットワークに対する攻撃」への防御機能が実装され始めています。Google Chrome 94では、XMLHttpRequestやFetch APIにより「パブリックネットワークからプライベートネットワークへの送信」については、HTTPSであることを要求するようになりました。

Starting in Chrome 94, public non-secure contexts (broadly, websites that are not delivered over HTTPS or from a private IP address) are forbidden from making requests to the private network.

Private Network Access update: Introducing a deprecation trial - Chrome Developers より引用

Chrome 94以降、パブリックな非セキュアコンテキスト(広義には、HTTPSで配信されていないウェブサイトや、プライベートIPアドレスからでないウェブサイト)は、プライベートネットワークへのリクエストが禁止されるようになりました(私訳)。

これにより、Google ChromeでDNSリバインディング攻撃しようとすると以下のようなエラーになります(ホスト名は例示用のもの)。

Access to XMLHttpRequest at 'http://example.com/secret.txt' from origin 'http://example.org' has been blocked by CORS policy: The request client is not a secure context and the resource is in more-private address space `private`.

エラーメッセージとしてはCORSエラーとなっていますが、DNSリバインディングは通常同一オリジンポリシーの範囲内のアクセスであり、当然CORSのエラーではありません。しかし、Google Chromeの最近のセキュリティ機能では、パブリックネットワークからプライベートネットワークへのリクエスト、およびプライベートネットワークからローカルホストへのアクセスについては、通常より厳しい制限を課していることになります。

このセキュリティ機能は、ローカルネットワークに対するCSRF緩和策として導入されたようですが、DNSリバインディング対策としても機能すると思われます。なぜなら、HTTPSによりDNSリバインディング攻撃しようとすると、攻撃先サーバーへのアクセスで証明書のエラーエラーになり、アクセスは停止するからです。

さらに、Google Chrome 102 以降では、このような状況でプリフライトリクエストが送信されるようになる予定です。

Preflight requests for PNA are also sent for same-origin requests, if the target IP address is more private than the initiator. This is unlike regular CORS, where preflight requests are only for cross-origin requests. Preflight requests for same-origin requests guard against DNS rebinding attacks.

Private Network Access: introducing preflights - Chrome Developers より引用

PNA(Private Network Access)のプリフライトリクエストは、ターゲットIPアドレスがイニシエーターよりもプライベートである場合、同一オリジンのHTTPリクエストに対しても送信されます。これは、プリフライトリクエストがクロスオリジンリクエストに対してのみ送信される通常のCORSとは異なります。同一生成元リクエストのプリフライトリクエストは、DNSリバインディング攻撃を防止します(私訳)。

引用元にもDNSリバインディング攻撃防止のためと明記されています。

このようにGoogle Chrome(あるいはChromium系ブラウザ)を使うことでDNSリバインディングの対策が強化されますが、これら対策はProxyサーバー経由での通信には適用されません。Proxy経由での通信の場合、ブラウザからはパブリックあるいはプライベートという区別がつかないからです。これはDNS Pinningについても同様であり、各ブラウザ共通の挙動になります。

③ Proxyサーバーでの対策

前述のようにProxy経由でのブラウザ利用の際には、ブラウザ側のDNSリバインディング対策は機能しないため、Proxy側での対策が求められます。よく理由されるFoward ProxyであるSquidには、以下のDNS Rebindin対策が利用できます。一方、Apache HTTPdをProxyとして使う場合、DNSリバインディングに使える機能は調査の範囲では見つかっていません。ご存じの方はぜひご指摘ください。

(1) IPアドレスによるアクセス制御

Squidのアクセス制御機能から、接続先(Destination)のIPアドレスを制限することができます。以下は、192.168.0.0/24に対するアクセスを拒否する例です。

acl localnet dst 192.168.0.0/24 http_access deny localnet

(2) DNS Pinning

SquidはDNSキャッシュの上下限を指定することができます。

ディレクティブ 意味 デフォルト値
positive_dns_ttl DNSキャッシュの上限 6時間
negative_dns_ttl DNSキャッシュの下限 1分

これらのうち、negative_dns_ttl はDNS Pinningの用途に使うことができます。 しかしながら、negative_dns_ttlをむやみに長くすると副作用もあります。もしも正規のDNSクエリに失敗した場合、その失敗の結果もキャッシュされるからです。また、前述のように、正常なIPアドレスが短期間で変わる場合もありえます。このため、negative_dns_ttlをデフォルトより長くすることはお勧めできません。

結論としては、SquidでDNSリバインディング対策する場合は、(1)のアクセス制御による方法がお勧めです。

【参考】

④ DNSキャッシュサーバーでの対策

リゾルバ(DNSキャッシュサーバー)による対策も可能です。Squidの項で説明したDNS Pinningとアクセス制御の両方が主要リゾルバで用意されています。

DNS Pinning

下表に主要ブラウザでDNS Pinningを指定するディレクティブを示します。DNSコンテンツサーバー側でTTL=0秒などと短い秒数が指定されていた場合でも、こちらで指定した秒数に切り上げられます。

リゾルバ bind9 unbound dnsmasq PowerDNS KNOT DNS
設定項目 min-cache-ttl cache-min-ttl min-cache-ttl minimum-ttl-override cache.min_ttl()
備考 90秒以下 制限など記載なし 1時間以下 デフォルト1秒 デフォルト5秒

PowerDNSとKNOT DNSは最短のTTLがデフォルトとして指定(1秒~5秒)されており、TTL=0は許容されない設定になっています。このような控え目の設定でもSSRF攻撃の緩和には役立つと思われます。

特定IPアドレスの拒否

また、多くのDNSサーバーは、DNSリバインディング対策として、IPアドレスのフィルタリング機能を提供しています。下表はIPアドレスの拒否リストあるいはDNSリバインディング対策として使えるプライベートIPアドレス等を拒否するためのディレクティブの一覧です。

リゾルバ bind9 unbound dnsmasq PowerDNS KNOT DNS
設定項目 deny-answer-addresses private-address stop-dns-rebind 設定は見つからなかった modules.load('rebinding < iterate')
備考 サブネットマスク等で指定 サブネットマスク等で指定 一括停止 Luaスクリプトで可能 一括停止、詳細にやるならLuaスクリプト

私の調査では、PowerDNSのみ該当する項目を探せなかったのですが、Luaスクリプトで特定IPアドレスを拒否することは可能でした。 また、dnsmasqとKNOT DNSはIPアドレス指定ではなく、DNSリバインディングに使われそうなサブネットマスクを一括して拒否する形になっています。

また、単体のリゾルバではありませんが、Googleが提供するGoogle Nest WiFiにはDNSリバインディング防御機能が提供されてます。

Google Nest スピーカー、ホームメディア サーバー、IoT(モノのインターネット)デバイスのような接続されたデバイスをホストするホーム ネットワークは、DNS リバインディングと呼ばれる攻撃を受けるおそれがあります。Google Wifi では、この種の攻撃を防止するため、DNS リバインディングに対する保護機能で、公開ドメインにプライベート IP アドレス範囲を使用されないようブロックすることができます。この機能はデフォルトで有効になっています。

DNS リバインディングに対する保護機能 - Google Nest ヘルプより引用

試したところ、192.168.10.1 のようなプライベートIPアドレスはブロックされる一方で、127.0.0.1 のようなlocalhostのアドレスはブロックされません。また、当該機能は副作用の可能性もあるため、無効にすることもできます。

リゾルバでのDNSリバインディング対策をどう考えるか

多くのリゾルバがDNSリバインディング対策の機能を提供しますが、デフオルトは無効化されていることから、いずれも「積極的に使っていきましょう」という姿勢ではないように感じます。その典型はbindです。min-cache-ttlの最大値は90秒なので「これで守り切る」という数字ではありません。また、IPアドレスの拒否リストについては以下のような注意書きがマニュアルに付記されています。

The “rebinding” attack must primarily be protected at the application that uses the DNS. For a large site, however, it may be difficult to protect all possible applications at once. This filtering feature is provided only to help such an operational environment; turning it on is generally discouraged unless there is no other choice and the attack is a real threat to applications.

4. BIND 9 Configuration Reference — BIND 9 9.19.0 documentation より引用

リバインディング攻撃は、主にDNSを使用するアプリケーション側で保護されなければなりません。しかし、大規模なサイトでは、可能性のあるすべてのアプリケーションを一度に保護することは困難な場合があります。このフィルタリング機能は、そのような運用環境を支援する目的にのみ提供されています。他に選択肢がなく、攻撃がアプリケーションにとって本当に脅威とならない限り、この機能を有効にすることは一般に推奨されません。(私訳)

この「DNSを使用するアプリケーション側で保護されなければなりません」という指摘に私も同意します。 また、以下の資料にはDNSリゾルバの防御機能の抜け穴が報告されています。例えば、CNAMEとしてlocalhost.を返すようなケースが紹介されています。

このため、DNSリゾルバでの対策はあくまで緩和策としてとらえておくべきだと思います。

結局DNSリバインディング対策はどうすればよいか

ここまで説明したように、DNSリバインディング対策が「出来る」ポイントは以下の4箇所あります。

  • 攻撃対象
  • ブラウザ
  • PROXYサーバー
  • リゾルバ

これらの中で、確実な対策がとれるのは攻撃対象のみです。対策自体は難しいものではないので、まずはこれを検討するべきです。 しかし、以下のような状況はありえます。

  • 社内ネットワーク内に攻撃対象となり得る機器が多すぎで把握すら難しい
  • 対象のサーバーや機器側の仕様上対策が難しい

このため、以下のステップで対策を検討するとよいでしょう。

  1. ネットワーク内に存在するDNSリバインディング攻撃対象の機器やパソコンの洗い出し
  2. 対策の要否検討
  3. 対策方法の検討
  4. 対策実施

DNSリバインディングはマイナーな攻撃方法であり、他のウイルス感染などの方が簡単に侵入できるため、あまり過敏になることもないとは思いますが、原理的には可能であるので、「重要情報が盗まれる」などの状況は放置しない方がよいでしょう。DNSリバインディングの確実な対策の一つが「認証ちゃんとやる」ですので、結局のところ、

  • 社内ネットワークといえども認証とアクセス制御をきちんとやる

この当たり前のことをちゃんとやっていれば大丈夫です。 後は、保険的な対策としてPROXYやリゾルバの対応をどうするかですが、近年はPROXYやリゾルバはセキュリティ製品の一機能である場合も多く、自前でオープンソースソフトウェアを構築するケースは少ないと思います。なので、PROXYやリゾルバでの対策は、積極的に活用するというよりは、「使える場合は使うことも考える」くらいのレベル感ではないでしょうか。

まとめ

DNSリバインディングの対策として使える機能について説明しました。Google社がDNSリバインディング対策に熱心なことが印象的ですが、まずは攻撃可能性の洗い出しと、ローカルネットワークでも認証をおろそかにしないという基本的な対策を推奨いたします。

2022年3月14日月曜日

とある通販サイトに学ぶ自動ログイン機能のバッドプラクティス

 サマリ

とある通販サイトにて「 メールアドレス・パスワードを保存する」機能がありますが、サイトにクロスサイトスクリプティング(XSS)脆弱性がサイトにあると生のパスワードが漏洩する実装となっています。本稿では、この実装方式を紹介し、なぜ駄目なのか、どうすべきなのかを紹介します。

記事の最後にはセミナーのお知らせを掲載しています。


はじめに

家人がテレビを見ていて欲しい商品があるというので、あまり気は進まなかったのですが、その商品を検索で探して購入することにしました。「気が進まない」というのは、利用実績のないサイトはセキュリティが不安だからという理由ですが、この不安は的中してしまいました。

最初の「えっ?」はパスワード登録のところでして、パスワードを再入力する箇所で「確認のためもう一度、コピーせず直接入力してください」とあるのですよ。私は乱数で長く複雑なパスワードを入力しかけていたのですが、コピペができないとなると長すぎるので、パスワードを短くしました。セキュリティ上は逆効果だと思うのですが、なぜこうするのでしょうかね。しかし、これは本題ではありません。


「パスワードを保存する」チェックボックスの存在

会員登録が終わってサイトにログインしようとすると、パスワード欄の下に下記のようなチェックボックスがあり、デフォルトはONになっています。

メールアドレス・パスワードを保存する

「ログイン状態を保持」とかならよくある機能ですが、「パスワードを保存」とは匂いますね。そこで、そのままログインをした後、ログアウト後にもう一度ログイン画面を表示させると、なんということでしょう! メールアドレスとパスワードが初期値として入っているではありませんか! 以下のようなHTMLがサーバー側で生成されていました。

<input name="email" value="tokumaru@example.jp">
<input type="password" name="pwd" value="P@ssw0rd">


「パスワードを保存する」機能の実装は?

こういう実装を見ると「サーバー側でパスワードが平文で保存されている」と思う人が多いようですが、そうではないようです。

メールアドレスとパスワードは、あるクッキー(ここではXとする)に紐づけられています。当初は、クッキーXをキーとしてサーバー側でパスワード等を保存しているのかと思いましたが、そうではなく、クッキーXにメールアドレスとパスワードが暗号化して保存されているようです。そう判断した理由は、メールアドレスとパスワードの長さを変えるとクッキーの長さも変わるからです。また、暗号化に際し初期化ベクトルも使ってないようです。


クッキーが漏洩すると平文パスワードまで漏洩する

「初期化ベクトルを使わないなんてダメじゃないか」と思いますよね。そうなんですが、このサイトの場合、もっとダメな理由があります。というのは、暗号化されたクッキーをセットしてログイン画面を表示させると、先に紹介したように、平文でメールアドレスとパスワードがHTML上に表示されるのです。つまり、暗号化していても簡単に平文に戻せるのですよね。なので、初期化ベクトル云々という次元ではなくなってしまっているわけです。

このサイトの場合、クッキーXにsecure属性はついていますが、HttpOnly属性はついていません。なので、サイト上にクロスサイトスクリプティング(XSS)脆弱性があると、クッキーは簡単に漏洩します


仮にHttpOnly属性がついていてもXSSで平文パスワードが漏洩する

しかし、仮にクッキーXにHttpOnly属性がついていても、XSS攻撃により平文のメールアドレスとパスワードは漏洩します。XSS攻撃によりXMLHttpRequestでログイン画面をリクエストすると、そのレスポンスのログイン画面HTMLにはメールアドレスとパスワードが平文で記載されているからです。

そもそもサイトにXSSがあると、クッキーにHttpOnly属性がついていても「なりすまし」により情報漏えいやサイト機能の悪用は避けられません。このあたりは以下の動画にて詳しく説明しています。

しかし、パスワードまで漏洩してしまうと、そのパスワードがパスワードリスト攻撃により悪用されたり、「パスワードを入力しないと利用できない機能」まで悪用できるので通常のXSSよりも被害が増大することになります。


この場合のベストプラクティスは?

ログインを簡便にするために、いったんログインした後はパスワードを入力しなくてもログインを継続したいというニーズ自体はよくあるものであり、拙著では、「5.1.4 自動ログイン」にて解説しています。

体系的に学ぶ 安全なWebアプリケーションの作り方 第2版 脆弱性が生まれる原理と対策の実践

本項では、自動ログインの危険な実装を紹介した上で、トークンを使うなど安全な自動ログインの実装方法について紹介しています。詳しくは上記書籍を参照してください。


サイトオーナーはどうすればよかったのか?

とある通販サイトの自動ログイン機能がイケてないことを紹介しましたが、このサイトの運営者はどうすればよかったのでしょうか。

まず思いつくのは「脆弱性診断はしていなかったのか?」ということです。脆弱性診断していたかどうかは外部の者には分かりませんが、以下の可能性が考えられます。

  • 脆弱性診断はしていなかった
  • 脆弱性診断はしていたが指摘されなかった
  • 脆弱性診断で指摘されていたが、改修はしなかった

最後のケースを考えると、仮に脆弱性診断で指摘されていたとしても、サイトが出来上がった後では簡単に修正できるものでもなく、いささか「手遅れ」という感があります。なので、サイトを実装する前に自動ログインのセキュリティを検討しておくべきでした。

自動ログインは珍しい機能ではなく、「徳丸本にも載っている」ようなよくある機能なのですから、ウェブアプリケーションのセキュリティガイドラインのようなものがあれば、安全な実装ができていたかもしれないと考えられます。このガイドラインには、自動ログインのような機能面だけでなく、SQLインジェクションのような実装面にも言及されているとよいですね。


宣伝

では、「徳丸本にも載っていないような機能」についてはどうすればよいでしょうか。こちらについては個別に検討するしかありません。その際に類似機能を持つ先行サイトを調べてもよいでしょうが、たまたま参考にしたサイトがセキュリティ的に強固な仕様であるとは限りません。

また、「そもそもセキュリティ上の問題があるのか」という脅威分析をしないことには、セキュリティを検討する・しないの俎上にも上がらないということになります。

ということから考えると、セキュアな開発にも到達度レベルがあって、以下のようになるのではないかと考えています。

今回紹介したケースは、脆弱性診断はしていたかもしれないが、指摘されたか否かは分からず、少なくとも危ない実装が修正されないまま本番リリースされたという意味で、「レベル1にも到達していない」と考えられます。

私は拙著を書く際に、ウェブアプリケーションに存在する問題を列挙してできるだけ多くのパターンを解説してしまおう、と目論みました。なので徳丸本はあんなに分厚いのですが、それでも全てのパターンを列挙することは当然できなません。なので、徳丸本をいくら読んでも到達できるのは上記の「レベル2」です。

私の最近の関心はその上のレベルに到達するための方法論です。これについては2022年3月16日のセミナーにて簡単に紹介する予定ですので、お時間がある方は参加いただけると幸いです(宣伝)。

  • 2022年3月16日(水)16:00~17:00(オンライン)
  • 講演タイトル:セキュア開発ライフサイクル(SDLC)実践入門
  • イベント詳細・お申し込みはこちらから

2022年1月26日水曜日

2022年1月においてCSRF未対策のサイトはどの条件で被害を受けるか

サマリ

2020年2月にGoogle ChromeはCookieのデフォルトの挙動をsamesite=laxに変更しましたが、2022年1月11日にFirefoxも同様の仕様が導入されました。この変更はブラウザ側でCSRF脆弱性を緩和するためのもので、特定の条件下では、ウェブサイト側でCSRF対策をしていなくてもCSRF攻撃を受けなくなります。この記事では、デフォルトsamesite=laxについての基礎的な説明に加え、最近のブラウザの挙動の違いについて説明します。

(2022年1月29日追記)
本日確認したところ、Firefoxにおけるデフォルトsamesite=laxはキャンセルされ、従来の挙動に戻ったようです(Firefox 96.0.3にて確認)。デフォルトsamesite=lax自体は先行してGoogle Chromeにて実装されていましたが、細かい挙動の差異で既存サイトに不具合が生じたようで、いったん巻き戻されたようです。
このため、Firefoxについての現状の挙動はSafari等と同じになります。以下、当面の間読み替えていただければと思います。

デフォルトsamesite=laxキャンセルの情報は、TwitterにてMasataka Yakuraさんから教えていただきました。以下のスレッドにて確認することができます。


(追記終わり)


Cookieのsamesite=laxとは 

Cookieのsamesite属性は、元々Google Chrome 51にて導入されたセキュリティ機能で、その後他の主要ブラウザにも導入されています。samesiteのとり得るパターンは、指定なし、none、lax、strictですが、本稿では主にsamesite=laxについて取り上げます。

Cookieにsamesite=laxを指定した場合、異なるサイトから遷移してきた場合にCookieが送信されるか否かは文脈によって変わります。下図の状況においては、example.jpに対してGETメソッドで遷移する場合はCookieが送信されますが、POSTメソッドの場合はCookieは送信されません。

GETリクエストとPOSTリクエストで挙動が変わるのは合理的な仕様です。例えば、gmailからtwitterにリンクをたどって遷移する場合はGETメソッドが使われるためtwitterにCookieが送信され、twitterをログイン状態で閲覧することができます。一方、罠サイトから投稿ページに偽投稿を送信する場合は、「更新処理にはPOSTメソッドを使う」という原則によりPOSTメソッドのはずなので、samesite=laxのクッキーはサイトに送信されず、ログイン状態にはならないためCSRF攻撃も成立しないことになります。
このように、Cookieのsamesite=lax指定は、サイト閲覧時の利便性と安全性のバランスがよく、優れた仕様であると考えられます。

デフォルトsamesite=laxの流れ

samesite属性がブラウザに導入された当初は、従来のサイトの互換性は維持されていましたが、Google Chorme 80において、samesite無指定時の挙動がsamesite=laxに変更され、Firefoxもバージョン96にて追随しました。

この変更により「サイト側でCSRF対策していなくてもブラウザ側にてCSRF防御する」ことが可能になります。この変更は過去のサイトの互換性を崩す大胆なものであるため議論を呼んだようですが、Google ChromeおよびFirefoxという主要ブラウザが対応したことにより、今後の主流となることが確定しました。

その他のブラウザはどうか

本稿執筆時点(2022年1月26日)において、Google Chrome、Edge、Opera、Firefox等主要ブラウザの多く(Chromium系およびFirefox)にてデフォルトでsamesite=laxとなっています。一方、IEおよびSafariはsamesite属性を実装しているものの、現時点ではデフォルトでsamesite=laxにはなっていません(参考)。また、iOS上のブラウザはすべてWebKitベースとなっているので、iOS上のChrome等もSafariと同じ挙動になります。


2022年1月においてCSRF攻撃を受ける条件

それでは、「サイト側でCSRF対策していない場合」において、CSRF攻撃を受ける条件はどうでしょうか。以下、ケース別に説明します。

古いブラウザを使っている場合

samesite属性未実装の古いバージョンのブラウザを使っているユーザーは、サイト側のsamesite属性の設定に関係なくCSRF攻撃を受けることになります。最近のブラウザは自動アップデートの機能を実装していますが、自動アップデートを無効にしていたり、スマホ版のブラウザのバージョンが古い、スマホのOSをアップデートしていないケースではsamesite属性が無視される場合があります。


最新のIEやSafariを使っている場合

利用者が最新のIEやSafariを使っている場合、デフォルトではsamesite=laxではないため、サイト側でCSRF対策しておらずsamesiteの指定がない場合はCSRF攻撃の影響を受けます。一方、サイト側で明示的にsamesite=laxを使っている場合はCSRF攻撃を受けません。ただし、サイト側で、「GETメソッドで更新処理を受け付ける」実装になっている場合は、GETメソッドを用いたCSRF攻撃を受けます。脆弱性診断でも、この「GETメソッドでCSRF攻撃を受けるサイト」は時々見かけます。更新処理はPOSTメソッドのみ受け付けるようにすべきです。


最新のGoogle ChromeやFirefoxを使っている場合

利用者が最新のGoogle ChromeやFirefoxを使っている場合、Cookieのデフォルトがsamesite=laxになっているため、samesiteが、「指定しない」、lax、strictのいずれかであり、かつGETメソッドによる更新処理を許容していない場合は、それだけでCSRF攻撃をブラウザが防ぎます。samesite=noneが指定されている場合は、異なるサイトからのPOSTメソッドでもCookieが送信されます(すなわちCSRF攻撃の影響を受ける)が、samesite=noneを指定する場合はsecure属性も指定する決まりになっています。

ただし、デフォルトsamesite=laxには「2分間ルール」というものがあり、現実的な可能性は低いものの、CSRF攻撃を受ける余地があります。

2分間ルール

最新のGoogle ChromeおよびFirefoxにおいて、samesite属性を指定しないCookieはsamesite=laxの扱いを受けますが、Cookieが生成されてから2分経過してからsamesite=laxになる仕様になっています。このため、ログイン処理などで新たにCookieが発行してから2分間であれば、CSRF攻撃の影響を受ける可能性があります。


2分間ルールの挙動の違い

さらに、Google ChromeとFirefoxでは「2分間ルール」の実装に違いがあるようです。

Google Chromeの場合、Cookieが新規生成されなくても、同名のCookieが上書きされた場合、2分間ルールが延長されます。これは、セッション固定攻撃対策などでセッションIDの値を再生成(PHPの場合はsession_regenerate_idを使う)した場合が該当します。

これに対して、Firefoxの場合、Cookieを同名で上書きした場合は2分間ルールは延長されず、新規にCookieが生成された時刻が起点になります。このため、脆弱性のデモなどで敢えて2分間ルールを使いたい場合は、いったんCookieを削除してから新規にCookieを生成することにより、2分間ルールの恩恵を受けることができます。

下図は上記挙動を図示したものです。時刻=1.5分のところでCookie SESSIDを再生成したところ、Google Chromeはsamesite=noneの時間が2分間延長されているのに対して、Firefoxは延長されていないこと、一旦Cookieを削除すると、どちらのブラウザでもsamesite=noneの期間が2分間復活することが図から見て取れます。

Google ChromeとFirefoxのこれら挙動は実験により確かめたもの(Google 97.0.4692.99、Firefox 96.0.2にて確認)なので、将来のバージョンアップなどで変更される可能性があります。


まとめ

2022年1月時点でのCSRFの影響について説明しました。ブラウザ側のセキュリテイ強化により、CSRF攻撃はかなり影響を受けにくくなっていはいますが、まだ「サイト側で何もしなくてもCSRF攻撃を受けない」状況ではありません。

引き続きCSRFの対策は必要であり、以下を推奨いたします。

  • アプリケーションフレームワークの提供するCSRF防御機能を使う
  • セッションIDのCookieにはsamesite=laxを明示する
  • 更新処理ではGETメソッドを受け付けないことを確認する

おわび・訂正

当初の版では「IEはsamesite属性に対応していない」と記載していました。これはMDNの記載を根拠としたものですが、実際にはIEの最新版ではsamesite属性に対応しています。本来であれば該当箇所を打消し線で訂正すべきところですが、煩わしくなるため、単に修正しております。


2021年12月20日月曜日

PHPにはエスケープ関数が何種類もあるけど、できればエスケープしない方法が良い理由

このエントリは、PHP Advent Calendar 2021 の20日目のエントリです。19日目は @takoba さんによる PHPプロジェクトのComposerパッケージをRenovateで定期アップデートする でした。

SQLインジェクションやクロスサイトスクリプティング(XSS)の対策を行う際には「エスケープ処理」をしましょうと言われますが、その割にPHP以外の言語ではあまりエスケープ処理の関数が用意されていなかったりします。それに比べてPHPはエスケープ処理の関数が非常に豊富です。これだけ見ても、PHPはなんてセキュアなんだ! と早とちりする人がいるかもしれませんが、しかし、他言語でエスケープ処理関数があまりないのはちゃんと理由があると思うのです。

本稿では、PHPのエスケープ処理用の関数を紹介しながら、その利用目的と、その関数を使わないで済ませる方法を説明します。


SQL用のエスケープ関数

セキュリティとエスケープといえば、真っ先に思いつくのがSQL文字列のエスケープです。関数名にescapeが含まれるものとして下記があります。これらのエイリアスもありますが、割愛します。

これらのうち、pg_escape_identifierは識別子のエスケープをする関数です。SQLの識別子の問題については、下記の連作を御覧ください。

結論として、SQL識別子のエスケープは、phpMyAdminのようなツールを開発する場合は必要になるが、一般のアプリケーション開発であれば識別子のエスケープをしなくてもすむようにした方がよい、というものです。

また、pg_escape_literalは、エスケープするだけでなく文字列を引用符で囲ってリテラルの形にしてくれるものです。PDOだと以下のメソッドが該当します。

単にエスケープだけするのに比べて、引用符で囲ってリテラルを生成する関数・メソッド(pg_escape_literalやPDO::quote)を使ったほうが安全です。これらquote系の関数は、対象が数値の場合も適切に処理してくれることが期待できるからです(ただし、伝統的にPDOは数値の扱いがイマイチですが)。

さて、SQLインジェクション対策にはエスケープが重要とはよく言われることですが、数値が対象の場合はエスケープしない(してはいけない)ことや、データベース・ソフトウェア毎にエスケープのやり方が異なるなど、現実には難易度の高い処理です。このあたりの事情についてはIPAの安全なSQLの呼び出し方をぜひご一読ください。

結論としては、リテラル中の文字列をエスケープするのではなく、プレースホルダを用いてSQLを呼び出すべきであり、さらに言えば、今どきのモダンなアプリケーション・フレームワークを使う場合は、フレームワーク付属のO/Rマッパーを使うので、現実のアプリケーション開発でSQLのエスケープ関数を利用するケースは多くはないと思います。


HTMLエスケープ

HTMLエスケープ、すなわち文字を文字参照に変換する関数には以下があります。

通常クロスサイトスクリプティング対策にはhtmlspecialchars関数を用いますが、こちらについてもフレームワークを使う場合はテンプレートエンジンの機能で自動エスケープさせるのが吉でしょう。

また、PHPはJavaScriptの文字列リテラルのエスケープについては提供していません。json_encodeでできないこともないですが、JavaScriptの動的生成は非常に難易度の高い処理なので、拙著2版では、カスタムデータ属性に値を書いて、それをJavaScriptから参照する方法を推奨しています。これにより、JavaScriptのエスケープではなくHTMLのエスケープで統一できます。


OSコマンド(シェル)

PHPは、system関数などOSコマンド呼び出しのために、シェル形式のエスケープ関数を提供しています。これは他の言語ではあまり見当たらず、比較的珍しい機能だと思います。

これらのうち、escapeshellcmdについては仕様にまつわる脆弱性があり修正も不可能ということで、使用禁止となっています。

警告
escapeshellcmd() はコマンド文字列全体に適用しなければなりません。 また、そうしたところで、まだ任意の数の引数を渡すことによる攻撃を許してしまいます。 単一の引数をエスケープするには、かわりに escapeshellarg() を使わねばなりません。
https://www.php.net/manual/ja/function.escapeshellcmd.php より引用

この脆弱性は、下記ブログ記事で私が報告したものです。

この脆弱性を見つけたきっかけは、拙著の初版を書いている際に、OSコマンドインジェクション対策としてエスケープ処理で本当に大丈夫だろうかと心配になり色々試している中で見つけたものです。心配になった理由は、シェルのエスケープ処理の難易度が高いためです。

そして、この心配はPHPMailerの脆弱性CVE-2016-10033という形で現実のものになりました。

さて、シェル形式のエスケープ処理関数は比較的珍しいと書きましたが、他の言語ではどうしているのでしょうか。実はもっと良い方法、すなわちシェルを経由しないコマンド呼び出しが提供されています。以下の記事で解説しています。

そして、PHP 7.4にて、ようやくシェル経由でないOSコマンド呼び出しが提供されました。

このため、今後はOSコマンドインジェクション対策としては、エスケープ処理ではなく、proc_openによる「シェル経由でないOSコマンド呼び出し」を用いるべきだと思います。この場合はescapeshellarg等によるエスケープ処理は必要ありません。


正規表現

PHPは正規表現用のエスケープ関数も提供しています。

これはどのような時に用いるかと言うと、正規表現パータン中に外部入力を含める場合です。そんなことあるのかと思ってしまいますが、これが原因でRCE(リモートコード実行)可能な脆弱性が混入した例があります。

なので、外部入力を正規表現パターンに含める場合はpreg_quoteを使いましょう…とは私は思いません。そんな危険な行為そのものを避けるべきだと思います。現に、上記で紹介したphpMyAdminの脆弱性(CVE-2013-3238)も、当該箇所で正規表現を使わない形で改修しています。詳細は上記ブログ記事を参照ください。

ということで、preg_quoteもよほどのことがない限り使わない関数だと私は思います。


URL

URL中に記号文字やマルチバイト文字を含める場合はパーセントエンコード(URLエンコード)しますが、これも一種のエスケープ処理と考えることができます。PHPで用意されているパーセントエンコードの関数には以下があります。

概ね似たような処理を行う関数が4種類もあるのがPHPらしいですが、通常はrawurlencodeを用いればよいでしょう。urlencodeの方は、空白を%20ではなくプラス記号にエンコードします。こちらはHTMLフォームのapplication/x-www-form-urlencoded形式ということでしょうが、その差が問題になるケースはあまりないと思います。

URL中で記号をエスケープしないとまずい理由は、クエリ文字列では = や & の記号が区切り文字として特別な意味を持つからです。たとえば、a=値という形式で、値が「b&c=d」だとすると、全体ではa=b&c=dとなって、値がbのみで千切れてしまいます。

このように、URLのエスケープ(パーセントエンコード)自体は必要な処理ですが、目的がクエリ文字列の組み立てである場合は、rawurlencode等よりも便利な機能があります。それは、http_build_query です。この関数は配列を引数として、クエリ文字列形式の文字列を返します。この過程でパーセントエンコードもしてくれます。特に、項目数が多い場合に便利ですが、単一の場合でもhttp_build_queryを使うことをお勧めします。一般的に、ミクロな処理の関数を組み合わせて使うよりも、マクロな機能の関数を用いた方がプログラムの意図が分かりやすくなり、バグ、ひいては脆弱性が入る余地が少なくなるからです。


LDAP

古来から知られたLDAPインジェクションという脆弱性があります。私自身は、LDAPを使った検索で「*」を指定したら全件マッチになった例は脆弱性診断で見つけたことがあります。また、LDAPのクエリの式を変更するという、文字通りのインジェクションもありえます。

LDAPのクエリに外部入力を含めることは十分考えられるため、エスケープが必要になる場合があります。PHPでは5.6から下記の関数でLDAPのエスケープを提供しています。


目的不明なエスケープ関数

以下の3つの関数はPHP4時代からある由緒正しいものですが、利用シーンがよく分かりません。

これらのうち、addcslashesはリファレンスに「C 言語と同様にスラッシュで文字列をクォートする」とありますが、スラッシュ「/」ではなくバックスラッシュ「\」ですよね。PHP言語でC言語のソースコードを生成する時に使うのでしょうか。

もっとわけが分からないのは、addslashesとquotemetaです。

addslashes 

エスケープすべき文字の前にバックスラッシュを付けて返します。 エスケープすべき文字とは、以下のとおりです。

  • シングルクォート (')
  • ダブルクォート (")
  • バックスラッシュ (\)
  • NUL (null バイト)

https://www.php.net/manual/ja/function.addslashes.php より引用

なんとなく、MySQLのエスケープルールを思い起こしますが、わざわざ以下のように「SQLインジェクション対策に使うな」と念押しがしてあります。

addslashes() 関数は、 SQLインジェクション を防止しようとして誤った使い方がされることがあります。 この関数を使うのではなく、データベース特有のエスケープ関数 および/もしくは プリペアドステートメントを使うようにしてください。

quotemetaの説明は以下のとおりで、正規表現のエスケープを連想しますが、

quotemeta 

文字列 str について、

. \ + * ? [ ^ ] ( $ )

の前にバックスラッシュ文字 (\) でクォートして返します

https://www.php.net/manual/ja/function.quotemeta.php より引用

正規表現用とするには、ハイフン「-」が抜けていますし、正規表現用にはpreg_quoteがあるので、そちらを使うべきです(そもそも正規表現パターンに外部入力を含めるなということはありますが)。また、Perlにも同名の関数がありますが、こちらは記号類をすべてエスケープするので仕様が異なります。ということで、quotemetaの使い道は謎です。ご存じの方は教えてください。


まとめ

PHPに多数用意されているエスケープ用関数について紹介しました。

エスケープすべき局面でエスケープを怠る、あるいはエスケープ方法が不適切だと脆弱性になります。そして、エスケープ処理はしばしば難しいのです。エスケープをなめてはいけない。そして、本稿を書くにあたってケッサクな例を見かけました。以下は富士通のInterstage Application Server Smart Repository運用ガイドからの引用ですが、

SDK(JNDI)の場合

 JNDIは注意が必要です。

 LDAP、JNDI、Java言語それぞれで、\(エンマーク(バックスラッシュ))文字を特殊文字と扱うためです。いくつか例を示します。

 \を含むcn属性を指定するとします。

a\b

 LDAPの規約により\をエスケープする必要があります。

cn=a\\b

 JNDIの仕様により、この名前を指定するために、それぞれの\をエスケープする必要があります。

cn=a\\\\b

 Java言語の仕様により、この名前を文字列リテラルとして指定するために、それぞれの\をエスケープする必要があります。

String name1 = "cn=a\\\\\\\\b";

 同様に、,(カンマ)、"(ダブルクォーテーション)の場合はそれぞれ次のようになります。

String name2 = "cn=a\\\\,b";
String name3 = "cn=a\\\\\"b";

 最初の例をJNDIで使用する場合は、次のように記述します。

String name = "cn=a\\\\\\\\b,o=Fujitsu\\\\, Inc.,c=jp";

多重のエスケープが必要なために、バックスラッシュを8個重ねる必要が生じています。

実は、私も類似の記事を過去に書いたことがあります。以下の記事ではバックスラッシュを12個重ねています。

ということで、まとめは以下の通りです。

  • エスケープすべき局面でエスケープを忘れると脆弱性になる(前提)
  • エスケープ処理は意外にややこしい(現実)
  • エスケープしなくてもよい書き方(SQLのプレースホルダ、シェル経由しないOSコマンド呼び出し等)があればそちらを採用しよう(お勧め)


2020年12月19日土曜日

PHPビルドの楽しみ、あるいはポケモンとしてのPHPについて

この記事はPHP Advent Calendar 2020の19日目です。18日目は@You-sakuさんのPHPでYoutubeのAPIを利用したいでした。


私は以前、PHPのすべてのバージョンを使う環境として、phpallphpcgiallmodphpallを紹介してきました。これらは、PHPのすべてのバージョンをそれぞれコマンドライン、CGI、mod_phpの形式で動作させるものです。
現在では、PHPのバージョンを切り替えて動作させる仕組みはphpenvphpbrew等何種類もありますし、php-buildという、PHPをビルドするというそのものずばりの仕組みも広く使われています。
しかし、短いPHPスクリプトをPHPの全バージョンで試すという私の使用法からは、phpall(およびその派生としてphpcgiall、modphpall)はメリットがあり、私は今もPHPのビルドを続けています。

そう、私は今も、PHPのすべてのバージョンのバイナリをCLI、CGI、Apacheモジュールの形式で持っているのです。そして、元々は目的があって始めたことなのですが、いまや、「PHPのすべてのバージョンをコンプする」こと自体が目的化してしまっています。この感覚、何かに似ているなと思い返したら…そうです。ポケモンを集めるような感覚なのです。いまや、

徳丸にとってPHPはポケモン


と言っても過言ではありません。
PHPがポケモンだという主張は認めていただくとして、PHPにもポケモン同様にレアキャラがあるでしょうか…

あります。ビルドが困難であればあるほど、レアなポケモン(に相当するPHP)と言ってよいでしょう。
経験上、以下の条件を満たすPHPがレアな(ビルドが困難な)ものと言えます。
  • 古いバージョンのPHP(PHP4、PHP5.0、5.1、PHP5.2等)
  • 新しい環境で動く(Ubuntu 20.04等)
  • できるだけ多くの拡張モジュールを備える
  • 32ビット版よりも64ビット版
以下、その理由を簡単に説明しましょう。

古いバージョンのPHP

次の項と関連しますが、古いPHPはビルドすること自体が困難です。その一端は@hnwさんの以下の記事で読むことができます。


古いPHPがビルドが困難な理由は以下の通りです。
  • Cコンパイラの新バージョンでは文法チェックが厳しくなった結果、コンパイルエラーになりコンパイルできない
  • PHPが利用するライブラリの非互換なバージョンアップ
これらの回避策としては以下が考えられます。
  • 古いコンパイラ(gcc)を使う
  • コンパイルエラーを緩和するオプションを設定する
  • 古いライブラリを使う
  • PHPにパッチをあてる
最終的にはPHPにパッチをあてざるを得ない場合もありますが、これはできれば避けたいところです。新しいgccでコンパイルエラーになる箇所を見ていると「これ、単にバグじゃないか」と思うものが多い(それ以外ではライブラリの非互換)のですが、他の方法で回避できるのであれば回避したい…ということで、古いgccを試したりしましたが、今は以下のgccのオプションで回避して、最低限PHPソースにパッチをあてています。
-std=gnu89
-fgnu89-inline
ライブラリも結構ハマるところでして、以下は複数のバージョンを使っています。
  • zlib(1.1.4、1.2.11)
  • openssl(0.9.8、1.1.1)
  • curl(7.15.0、7.16.0、7.68.0、7.72.0)
  • libxml2(2.7.8、2.9.8、2.9.10)
意外にハマるのがcurl(libcurl)でして、非互換なバージョンアップのため、PHPのバージョンによってcurlのバージョンも変える必要がありました。

新しい環境で動く

前項の裏返しになりますが、新しい環境(OS)であるほど、古いPHPをビルドすることが困難になります。特にライブラリの非互換は避けようのないところで、OS(Linuxディストリビューション)にバンドルされているライブラリでうまくビルドできない場合は、古いライブラリを探してきてビルドすることから始めなければなりません。ライブラリ毎にビルド方法に癖があったりして苦労する場合もありますが、そこがまた楽しいのです。

できるだけ多くの拡張モジュールを備える

前項と関連しますが、できるだけ多くの拡張モジュールを指定した方がビルドの難易度が上がります。OSにバンドルされていないライブラリや、バンドルはされているが非互換のため古いライブラリを別途導入しないケースはなおさらです。

32ビット版よりも64ビット版

意外なことを書くと思われるかもしれませんが、古いPHP(4.1、4.2あたり)はAMD64でのビルドが困難です。ビルド自体はできるのですが、正常に動作しません。この時代はまだAMD64の現物がなかった(AMD64の出荷は2003年4月)からと思われます。PHP 4.0はビルドでき動作もするので、調整すればPHP 4.1や4.2も動作すると思うのですが、私はまだ成功していません。

phpallをUbuntu16.04(32ビット)からUbuntu20.04(64ビット)に移行

さて、著者はphpallの環境を当初Ubuntu12.04(32ビット)に構築し、その後Ubuntu16.04(32ビット)に移行していましたが、PHP 8.0α版が出た際にUbuntu20.04(64ビット)に移行しました。
そのきっかけとなったのは、PHP 8.0から導入されたJITを試そうとして時のことです。どうもx86(32ビット)では、JITは使えないようなのです。下図はphpinfoからの該当部分です。


JITのRFCではx86もサポートされていると書かれていましたが、本番投入は見送られたのでしょうか。私は、phpallを64ビット環境に移す決断をしました。以下はUbuntu20.04(64ビット)でのphpinfoで、JITが有効化できていることがわかります。


残る課題

楽しみとしてのPHPビルドについて紹介しましたが、現状課題が残っています。前述のように、64ビット環境でのPHP4.1と4.2のビルドには成功していないからです。ここは私の技術の至らなさではあるのですが、将来の楽しみにとっておこうと思います。やり方をご存知の方がいれば教えていただけるとありがたいです。

フォロワー

ブログ アーカイブ