Discourseで発見したCache Poisoning
Discourseに報告された脆弱性を眺めていたらCVE-2024-47773が目に止まったので調べているとその過程で悪用可能な問題を2件発見したのでDiscourseに報告しました。
CVE-2024-47773
zereさんによって報告されたこの脆弱性は、未認証状態でサイトに訪れた場合に利用されるアプリケーションキャッシュ(Annonymouse Cache)の機能を悪用したCache Poisoningでした。
CVE-2024-47773のパッチでCacheキーの生成にXHRリクエストかどうかの判定が追加されていました。
is_xhr = @env["HTTP_X_REQUESTED_WITH"]&.casecmp("XMLHttpRequest") == 0 ? "t" : "f"
@cache_key =
+"ANON_CACHE_#{is_xhr}_#{@env["HTTP_ACCEPT"]}_#{@env[Rack::RACK_URL_SCHEME]}_#{@env["HTTP_HOST"]}#{@env["REQUEST_URI"]}"
実際、Discourseはpreload_json
により、XHRリクエストやGETリクエストかどうかレスポンスに差異が生じます。
https://github.com/discourse/discourse/blob/v3.3.2/app/controllers/application_controller.rb#L436-L450
# If we are rendering HTML, preload the session data
def preload_json
# We don't preload JSON on xhr or JSON request
return if request.xhr? || request.format.json?
# if we are posting in makes no sense to preload
return if request.method != "GET"
# TODO should not be invoked on redirection so this should be further deferred
preload_anonymous_data
if current_user
current_user.sync_notification_channel_position
preload_current_user_data
end
end
同じURLに対するリクエストでも、XHRリクエストとナビゲーションリクエストでレスポンスに差異があるため、XHRリクエストのレスポンスをキャッシュさせることでCache Poisoning DoSを起こしていたことが分かります。
このコードを眺めていた際に、preload_json
とCVE-2024-47773
のパッチでXHRリクエストの判定方法が異なることに気づきました。
CVE-2024-55948
CVE-2024-47773
パッチでのXHRリクエスト判定
@env["HTTP_X_REQUESTED_WITH"]&.casecmp("XMLHttpRequest") == 0 ? "t" : "f"
preload_json
でのXHRリクエスト判定
request.xhr?
Railsのxhr?
メソッドの実装は以下の通りです。
rails/actionpack/lib/action_dispatch/http/request.rb
request.xhr?
# aka
def xml_http_request?
/XMLHttpRequest/i.match?(get_header("HTTP_X_REQUESTED_WITH"))
end
alias :xhr? :xml_http_request?
ということで、X-Requested-With: xxxXMLHttpRequest
のようなヘッダーを付与するとpreload_json
ではXHRリクエストとして判定されますが、Annonymouse Cacheの判定ではXHRリクエストではないとして扱われます。
DiscourseはXHRリクエスト用のレスポンスを返しますがAnonymouse CacheはこのヘッダーはナビゲーションリクエストとしてキャッシュしてしまうことでCache Poisoningが可能でした。
CVE-2024-55948
Annonymouse Cacheの実装では、cacheable?
メソッドを使用してレスポンスをキャッシュするかどうかを決定します。
cacheable?
メソッドの中でget?
メソッドが呼ばれています。
https://github.com/discourse/discourse/blob/v3.3.2/lib/middleware/anonymous_cache.rb
def cacheable?
!!(
GlobalSetting.anon_cache_store_threshold > 0 && !has_auth_cookie? && get? &&
no_cache_bypass
)
end
get?
の実装ではRackのREQUEST_METHOD
環境変数の値を確認していました。
def get?
@env["REQUEST_METHOD"] == "GET"
end
REQUEST_METHOD
環境変数はX-HTTP-Method-Override
ヘッダを使うことでメソッドを上書きできるようになっています。
rack/lib/rack/method_override.rb
def call(env)
if allowed_methods.include?(env[REQUEST_METHOD])
method = method_override(env)
if HTTP_METHODS.include?(method)
env[RACK_METHODOVERRIDE_ORIGINAL_METHOD] = env[REQUEST_METHOD]
env[REQUEST_METHOD] = method
end
end
一方で、preload_json
メソッドは、request.method
に基づいてpreloadするかどうかを決定します。
request.method
はメソッドオーバーライドの影響を受けず、元のHTTPリクエストメソッドを返します。
この挙動を悪用してCache Poisoningが可能でした。
POST / HTTP/1.1
[...]
X-HTTP-Method-Override: GET
[...]