去年、今年とStruts2、Drupalのリモートコード実行脆弱性が問題になりました。記憶に新しい方も多いと思います。Railsにもリモートコード実行脆弱性が複数レポートされており、Railsユーザーであればよくご存知だと思います。
ざっと思い付く昔の脆弱性から最近の脆弱性まで簡単にまとめてみます。
evalやexec
Rubyのコードを実行する機能です。当たり前ですが、ユーザー入力を許可し、入力データをバリデーション&エスケープしていないとRubyコードが実行されます。
Kernel.execが攻撃に使えるとコマンドが自由に実行できます。(OSコマンド実行脆弱性)Rubyコードの作成も自由自在なので、Rubyコードの実行にも利用できます。
RubyもUNIX系とWindowsをサポートしており、コマンドラインのシェルが異なります。実行時に利用されるシェルの種類が確実に同じであると想定できない場合も少なくありません。エスケープだけではリスクが高いので”エスケープだけ”、ではなく検証済みの入力データを利用してコマンドラインに出力する必要があります。参考
SQLインジェクションも、条件がかなり厳しく限定されますが、場合によってはファイルの作成やコード/コマンドの実行に使える場合もあります。
画像へのコード埋め込みはPHPでよく利用される攻撃方法です。コード埋め込みの制限が厳しいだけです。例えば、GIF形式などでファイルヘッダーのみチェックしているバリデーションだとコードだとRubyでもコード埋め込みが可能になり、リモートコード実行に利用できます。(攻撃コードを隠す目的に利用することが可能)
外部入力からのクラス作成
klass = params[:class].classify.constantize
klass.do_something_with_id(params[:id]) if klass.respond_to?('do_something_with_id')
こういった感じのコードはRailsではよく見かけます。これでは”どんなクラスでも使えてしまう”のでリモートコード実行を許してしまう場合があります。
”どんなクラスでも使えてしまう”脆弱性はJavaなどリモートコード実行に使われる脆弱性です。
対応策は厳格なホワイトリスト型の入力バリデーションです。
klass = params[:class].classify
if %w(Class1 Class2 Class3).include? klass
klass.constantize.do_something_with_id(params[:id])
else
raise 'Forbidden'
end
外部入力からのメソッド呼び出し
method = params[:method]
@result = User.send(method.to_sym)
外部入力の値を使ってメソッドを呼び出しています。これもRailsでは割とよく見かけるコードです。”どんなメソッドでも使えてしまう”ことを利用したリモートコード実行が可能になる場合があります。
“どんなクラスでも利用できてしまう”脆弱性と同じく、他の言語のプログラムでも見かける脆弱性ですが、Rails/Rubyにはこのタイプのコードが多いと思います。
対応策は厳格なホワイトリスト型の入力バリデーションです。
method = params[:method] == 1 ? :method_a : :method_b
@result = User.send(method, *args)
どのクラスかつどのメソッドでも呼べてしまう場合
この場合の危険性は説明の必要もないと思います。アプリケーションで初期化できるどのクラスのどのメソッドでも呼べてしまうと、任意コード実行が可能なコードを呼べてしまう可能性が飛躍的に上がります。この状態は未検証入力をデシリアライズする状態と変りありません。
Rubyは仕様としてデシリアライズを安全に処理する責任を持ちません。つまり何が起きても使った開発者の責任です(これはPHP/Pythonなども同じ)任意のシリアライズデータをデシリアライズする状態と変わりません。(後で記述するMarshalを参照)
YAML.loadで任意コード実行 – CVE-2013-0156
これはCVE番号から分かるように古い脆弱性です。
脆弱なRails(2.x/3.x)に対してPOSTメソッドでContent-Typeをapplication/xmlに設定して以下のRubyコードを含むデータを送ります。
<?xml version="1.0" encoding="UTF-8"?>
<bang type="yaml">--- !ruby/object:Time {}
</bang>
コードが実行されます。
Parameters: {"bang"=>1969-12-31 18:00:00 -0600}
JSONデータを使ったリモートコード実行 – CVE-2013-0333
これはCVE番号から分かるように古い脆弱性です。前のYAML.loadで任意コード実行と同時期に見つかった問題で異なる脆弱性です。
タイトルの通りJSONでリモートコード実行ができます。対策コードにはバリデーションコードが追加されていることが分かります。
つまり対策は厳格な入力データバリデーションです。
JSONなどの処理は基本的にはライブラリに任せる事になりますが、アプリケーションプログラマが何もできない(しなくても良い)訳ではありません。
- データの長さが妥当であるか
- 使われている文字が妥当であるか(制御文字はあり得ない)
- 使われている文字エンコーディングが妥当であるか
これらどのような文字列データであってもバリデーション可能です。
renderで任意コード実行 – CVE-2016-2098
これはCVE番号から分かるように古い脆弱性です。
class TestController < ApplicationController
def show
render params[:id]
end
end
見るからに危いコードなので普通は書かないと思いますが、上記のコードで任意コード実行が可能です。
修正前のRailsでも厳格なホワイトリストによる入力バリデーションを行っていれば攻撃されません。以下のように、バリデーションを行うコードを追加します。
def show
render verify_id(params[:id])
end
private
def verify_id(id)
# add verification logic particular to your application here
end
直接renderを呼ぶリモートコード実行 – CVE-2016-0752
CVE-2016-2098脆弱性と類似の問題ですが、内容は異なります。
def show
render params[:template]
end
見るからに危険な香りがするコードで普通は書かないですが、任意コード実行が可能となる問題です。簡単にアプリケーションのRubyソースコードの内容を見れてしまいます。シークレットキーなども覗き放題です。
対策はこれも厳格な入力データバリデーションです。
def show
template = params[:id]
valid_templates = {
"dashboard" => "dashboard",
"profile" => "profile",
"deals" => "deals"
}
if valid_templates.include?(template)
render " #{valid_templates[template]}"
else
# throw exception or 404
end
end
ところで、この脆弱性は2015年2月1日にレポートされ、修正されたのは1年後の2016年1月25日になります。
Cookieとデシリアライズを使ったリモートコード実行
Ruby自体が任意データのデシリアライズによる問題はユーザーの責任、つまりユーザー入力をデシリアライズして何が起きても知りません、としています。なのでこれは脆弱性ではないですが、TrendMicroが脆弱性として公開しているので記載します。
Railsはシリアライズ後に暗号化し、それをクッキーとして送信します。暗号鍵情報を知っていれば、誰でも正しく暗号化できます。なので攻撃者が暗号鍵情報を知っていれば任意コードが実行できます。
暗号化キーが漏れている時点でどうなのか?といういう話もありますが、鍵が漏れるとデータを暗号化/復号化できるだけではなく、Railsの仕組みを使ってコード実行も出来てしまう、という点をTrendMicroは危険であると評価したのだと思われます。(で、どうしろと?という話になります。鍵は安全に管理しましょう、時々変えましょう、となります)
先に紹介したCVE-2016-0752を利用すると秘密の鍵情報を簡単に盗めます。
Marshalによるリモートコード実行
Railsがクッキーをデシリアラズすることによりコード実行ができる問題の本質は未検証データのデシリアライズです。
def reset_password
user = Marshal.load(Base64.decode64(params[:user])) unless params[:user].nil?
if user && params[:password] && params[:confirm_password] && params[:password] == params[:confirm_password]
user.password = params[:password]
user.save!
flash[:success] = "Your password has been reset please login"
redirect_to :login
else
flash[:error] = "Error resetting your password. Please try again."
redirect_to :login
end
end
Marshal.loadを未検証のユーザー入力に使ってはならないのは「仕様 」です。上記のようなコードは書かない様にします。
Renderのinlineによるリモートコード実行
inlineオプションはそもそもRubyコードを実行する機能です。危険な使い方をすると、当然ですが、Rubyコードが実行されます。
inlineは以下のように使います。
render inline: "<% products.each do |p| %><p><%= p.name %></p><% end %>"
入力バリデーションなしで識別子が埋め込まれていたり、文字列データがエスケープなしで埋め込まれていたりするとコード実行脆弱性が生まれます。evalなどと同じです。
既に紹介したCVE-2016-2098はActionPackの中にこの問題があった事例です。
おまけ1 – SQLインジェクションできるがRailsの脆弱性でない問題
任意のRubyコード実行ができる脆弱性ではなく、SQLインジェクション脆弱性問題を紹介します。
CVE-2017-17916、CVE-2017-17917、CVE-2017-17918、CVE-2017-17919、CVE-2017-17920はSQLインジェクションが出来てしまうとして報告されたCVEです。しかし、Railsの脆弱性として取り扱われていません。
対象となるRails APIは信頼できない入力を安全に処理する仕様ではないからです。ユーザーの責任として入力データをバリデーションしてから使う必要があります。厳格な入力データバリデーションが対策になります。
参考:要するに、APIを使えば良い、ではなく以下のような考え方で使わないと安全性は保てない、ということです。バリデーションには大別して3種類のバリデーションがあります。未検証入力が攻撃可能となる致命的な脆弱性の原因となることは上記のRails脆弱性からも明らかです。
おまけ2 – SAML認証の成り済まし
2018年2月に報告されたSAML認証の成り済まし脆弱性はRubyにも影響した問題です。以下のようなリクエストで成り済ましが可能でした。
<NameID>user@example.com<!—->.evil.com</NameID>
SAMLはエンタープライズ環境でよく利用される認証方式ですが、上記のように未入力検証脆弱性で簡単に成り済ましが可能でした。詳しくはブログにしています。
エスケープやXMLコメントのサニタイズ(除去)でも攻撃は防止できますが、遅すぎる対策でありフェイルセーフ対策(=本来頼ってはならない対策)です。
まとめ
未検証入力の存在によりリスクが高くなることが理解ります。攻撃防止にも入力バリデーションが必要ですが、そもそもプログラムが正しく動作する為には妥当な入力が必須です。未検証の入力で誤作動したり、遅すぎる時点でエラーになったり、攻撃されてしまうのは当然です。
ほとんどのRailsアプリケーションは入力データバリデーションが非常に甘いです。ISOやCERTが求めるレベルの入力データバリデーションを行っていれば、回避できる問題が多数あります。しかし、RailsのMVCアーキテクチャーはFail Fast原則(できる限り早くバリデーション)による入力データバリデーションではなく、モデルでのバリデーションが基本です。これにより未検証入力による脆弱性が入り込み易くなっています。問題がある判ってから対応までに1年を必要としたCVE-2016-0752に脆弱なコード
class TestController < ApplicationController
def show
render params[:id]
end
end
上記のような脆弱なコードにも対応できる入力バリデーションを行う適切な場所はどこか考えると、モデルやビューではない事は明らかです。Fail Fast原則とRailsの構造に従うと、コントローラーでバリデーションを行うのが最適です。
Railsセキュリティガイドはホワイトリスト型のデータ検証を繰り返し強く推奨しています。しかし、残念ながらコンピューターサイエンスとエンジニアリングのベストプラクティスと言える入力値検証の解説にはなっていません。
(strong parametersを拡張し入力値を全て定義&検証できるような仕組みに作り変えるのが良いのでは、と思っています。参考)
ISOが求めるレベルの入力データバリデーションを行わない脆弱性によりセキュリティ問題が発生し、損害賠償問題が発生したすると開発側が非常に不利な立場になります。アプリケーションの入力データ検証の責任は、当然ですがアプリケーションにあります。アプリケーションにデータ検証があれば防げる場合で、未検証データが原因でアプリケーションから情報漏洩が起きた場合、アプリケーションの責任と考えられます。
GDPR対象データを手続きなしで日本に持ち込めるようになります。この為、GDPR制裁リスクをEU加盟国でない日本であっても考慮しなければならない時代になっています。
CVEのまとめサイトを一見するとRailsにはリモートコード実行脆弱性が無いようにも見えます。しかし、実際にはフレームワーク自体にも脆弱性があったり、ユーザーコードによるリモートコード実行が可能になるケースが少ないとは言えません。