PHPer向け、Ruby/Railsの落とし穴

(Last Updated On: 2018年8月13日)

Railsアプリケーションを作る機会も多くなったと思います。今までPHPのみを使ってきた方の為に、開発者がよく落ちてしまうRails/Rubyの落とし穴を少しだけ紹介します。RailsからWebアプリをはじめる方にも役立つと思います。

to_iやto_fは型変換ではない

私のブログ読者はPHP開発者の方が多いと思うので基本的なところから紹介します。RubyはPHPよりデータ型に厳格です。例えば、””(空文字列)や0は偽(false)と評価されません。

2.0.0p247 :001 > true == true
 => true 
2.0.0p247 :002 > true == false
 => false 
2.0.0p247 :003 > "" == true
 => false 
2.0.0p247 :004 > "" == false
 => false 
2.0.0p247 :005 > 0 == true
 => false 
2.0.0p247 :006 > 0 == false
 => false

基本的にデータ型が一致しなければ、真とは評価されません。数値も同じで数値文字列と数値型(整数と浮動小数点)もデータ型が一致しなければ真と評価されません。そこで型を”一時的”に変えるto_i(整数型)、to_f(浮動小数点型)があります。

2.0.0p247 :001 > v = "123"
 => "123" 
2.0.0p247 :002 > v == "123"
 => true 
2.0.0p247 :003 > v == 123
 => false

文字列の”123″と整数の123は比較するとfalseと評価されます。

Webアプリケーションは基本的に全てテキストなのでこれでは困ります。to_iを使えば、文字列を整数として評価できます。

2.0.0p247 :004 > v
 => "123" 
2.0.0p247 :005 > v.to_i == 123
 => true

これで目出度し、目出度し、となれば良いのですがto_iは型変換ではありません。一時的に型を変えているダケです。元のデータはそのまま残っています。

2.0.0p247 :005 > v.to_i == 123
 => true 
2.0.0p247 :006 > v
 => "123"

“123”と表示され文字列となっている事が分かります。これがなぜ落とし穴になってしまうのか、というと

2.0.0p247 :007 > v = "123 <script>alert(1)</script>"
 => "123 <script>alert(1)</script>" 
2.0.0p247 :008 > v.to_i == 123
 => true 
2.0.0p247 :009 > v
 => "123 <script>alert(1)</script>"

この様に元のデータはそのまま残っているので型変換と勘違いして、そのまま出力してしまうと脆弱性の原因になってしまいます。

2.0.0p247 :010 > i = v.to_i
 => 123 
2.0.0p247 :011 > i
 => 123

違う変数に保存すればデータ型も当然変わります。to_iやto_fをしたから数値型のデータのハズ!と思っていたら間違えてしまうので危険です。

to_iやto_sは型変換ではない、と理解しておきましょう。

また無駄なto_sは要注意です。

2.0.0p247 :001 > v = "123 abc"
=> "123 abc" 
2.0.0p247 :002 > v.to_s
=> "123 abc"

元々文字列型のデータにto_sしても、当然と言えば当然ですが、問題なく実行されます。ライブラリの中などでこれがあると、数値型を文字列型に変換した上で連結ように見えます。見ての通り文字列部分は、当然ですが、残ったままです。文字列連結した上でhtml_safeを設定してしまうと脆弱性の発見が困難になります。

Railsは手っ取り早く安全にアプリを作るために使っているのに面倒くさいことはしたくない、という場合には、書き方はともかく

2.0.0p247 :003 > v.to_i.to_s
=> "123"

としてしまう事も可能です。

本当に形式として整数型や浮動小数点型として大丈夫なのか、チェックするには

2.0.0p247 :009 > n = Float(v) rescue nil
 => nil 
2.0.0p247 :010 > n = Integer(v) rescue nil
 => nil

のようなコードで確認できます。“123 abc”は不正なので両方共nilになっていることが上記の例から分かります。数値として正しい値の場合、もちろん数値になります。

2.0.0p247 :017 > v = "123"
 => "123" 
2.0.0p247 :018 > n = Integer(v) rescue nil
 => 123 
2.0.0p247 :019 > n = Float(v) rescue nil
 => 123.0
2.0.0p247 :020 > v = "123e10"
 => "123e10" 
2.0.0p247 :021 > n = Float(v) rescue nil
 => 1230000000000.0 
2.0.0p247 :022 > n = Integer(v) rescue nil
 => nil

後、知っておくべきはRubyでは0、””などfalseやnilで無いものは常に真である事ですが、これはプログラムを作れば嫌でも分かるので間違える事はほぼ無いと思います。

 

文字列データの開始と終端の正規表現は^,$ではない

PHPのpreg_*()もmb_regex_*()も文字列データの開始と終端はそれぞれ^と$です。しかし、同じような動作をする正規表現をRubyで書く場合、開始と終端はそれぞれ\Aと\zになります。正規表現を使ったバリデーションをする場合、^と$を使ってしまうとPHPのバイナリセーフでないPOSIX互換正規表現(ereg関数)を使った時と同じ脆弱性を作ってしまいます。(\Zもあります。詳しくはマニュアルを参照)

Rubyの正規表現では\Aと\zが文字列型に保存された先端と終端であることを解説していないページも多いようです。(将来の為の備考:少なくともこのエントリを執筆した時点で解説がないです)なかなか分かりづらい落とし穴かも知れません。Railsセキュリティガイドには明確にセキュリティリスクであると解説してあります。

/^https?:\/\/[^\n]+$/i

のようにhttps: プロトコルであることをチェックするバリデーションコードは無力です。

javascript:exploit_code();/*
http://hi.com
*/

のような入力で簡単にバリデーションをすり抜け

link_to "Homepage", @user.homepage

のようなコードでJavaScriptを実行されてしまいます。正しくバリデーションするには

/\Ahttps?:\/\/[^\n]+\z/i

のように書きます。設定で緩和することも可能ですが、Rubyのコードを書く場合には文字列型データの先端と終端にマッチさせる場合は常に\Aと\zを利用すると良いでしょう。

PHPのmb_regex()はRubyと同じOnigurumaライブラリを使っている、と知っている方も多いと思います。同じOniguramaライブラリなのでPHPと同じ、と思って勘違いしているケースが多くあります。

追記:PHPの正規表現の注意点はまだ先のエントリで書く予定でしたが、PHPは随分前に正規表現が行ベースだという事が問題になって、mbregexの方は^と$は内部的に\A, \Z(\zでなく\Z)に置き換えられています。(”\Zもあります”は伏線だったのですが追記で書いてしまいます)pcreの方も同様になっています、$では最後の改行にはマッチします。

$ 検索対象の終わりあるいは終端の改行文字の前

http://jp1.php.net/manual/ja/regexp.reference.meta.php

これが困る場合もあります。改行インジェクションはHTTPヘッダーインジェクションやメールヘッダーインジェクション脆弱性の原因になります。

\A検索対象文字列の始端(複数行モードとは独立)

\Z検索対象文字列の終端、または終端の改行(複数行モードとは独立)

\z検索対象文字列の終端(複数行モードとは独立)

http://jp1.php.net/manual/ja/regexp.reference.escape.php

ということで、入力バリデーションに正規表現を正しく使わないと意図しない問題の原因になります。ご注意ください。基本的に正規表現の必要性がないものは文字列関数でバリデーションすると良いです。正規表現で全体をマッチさせたい場合、PCREでもOnigurumaでも\A, \zを使っていれば問題ありません。なぜこんな仕様なのか書こうかと思いましたが、なんだがRuby/Rail用のエントリの追記でPHPの事を書きすぎたので、バリデーションのところまで来たらまた詳しく紹介します。

 

erbは万能ではない

PHPのテンプレートでもデフォルトでHTMLエスケープしてくれる物も多いので、これはRailsだけでなく全てのWebアプリケーションに共通と言えます。デフォルトでエスケープしてくれることは安全性向上に非常に役に立ちます。

しかし、デフォルトでHTMLエスケープする仕様は万能ではありません。例えば、

<tag attr=<%= foo %> >

というコードはHTML5などでは有効なHTMLを生成します。(HTML5ではクオート無し属性値もOK)これは非常に危険です。fooの値が

123 onMouseOver=alert(1)

ならHTMLエスケープは無力です。

さすがにこんなミスはほとんどしない、と思うかも知れません。JavaScriptコンテクストの中では似たようなミス、数値だという前提でクオートなし、は結構見かけます。当然ですがJavaScript文字列を出力する場合、JavaScript文字列用のエスケープが必要です。(参考:JavaScript文字列のエスケープ

タグ属性値を直接書く場合、必ずクオートで囲んで書くようにすると良いです。

タグの属性名はクオートで囲めないため<%= %>で出力しても何の効果もありません。この場合、確実にバリデーションするしかありません。プリペアドステートメントを利用している場合で、識別子エスケープが利用できない場合のSQLインジェクション対策に似ていますね。(参考:SQL識別子のエスケープ

標準のHTMLヘルパーの属性名にも同様な注意が必要です。タグの属性名には普通シンボル(文字列定数のような物をイメージしてください)を使います。シンボルを使っていればJavaScriptインジェクションに注意する必要がありません。属性名は”文字列”でも構いません。(最新のコードは確認していませんが、少なくとも許可されている)属性名がユーザー由来の”文字列”である場合、JavaScriptインジェクションに脆弱になります。私の知る限りではマニュアルには記載がないようです。マニュアルに記載がないからといって安全とは限りません。セキュリティ的に重要な部分の仕様はコードで確認すると良いでしょう。

HTMLコンテクストからJavaScriptコンテクストの文字列(<script>タグの中、onclick属性値など)へのエスケープはHTMLエスケープでも役立つ事もありますが、HTMLパーサーからJavaScriptパーサーにデータが渡された時点でも安全であることを保証しなければなりません。つまり、JavaScriptコンテクストへの出力などはJavaScript文字列のエスケープで紹介しているように、HTMLでもJavaScriptでも安全に処理されるエスケープ方法でエスケープした文字列を使うと良いです。

ヘルパーを使っていれば適切に処理してくれる物もありますが、URLエンコードが必要な場所にはURLエンコードが必要です。

erbはコンテクストを認識した自動エスケープをサポートしているテンプレートシステムではありません。ほとんどのPHPのテンプレートシステムと同様にコンテクストに応じたエスケープが必要です。ヘルパーAPIの実装・仕様にも注意してください。

 

register_globals=Onのようなモデル

RailsのモデルはPHPerお馴染み(?)のregister_globals=Onのような脆弱性があります。マスアサイメント(Mass Assignemnt)という名前の脆弱性として知られています。GitHubもマスアサイメントに脆弱だったのでご存知の方も多いと思います。

Rails4未満の古いRailsでもStrong Parametersによるホワイトリスティングが可能なので利用しましょう。

 

モデルでのバリデーション

PHPフレームワークの場合、入力バリデーションをコントローラーで行えるような仕様・仕組みになっている物も多いです。しかし、Railsはモデルでバリデーションするようになっています。よく分からないけどRailsが何となく上手く処理してくれているのだろう、と思っていると全くバリデーションや必要なエスケープ処理がないコードを書いてしまうケースがあります。何となく上手く処理してくれるような事はない、と考えましょう。

インターネットに載っている例も疑うべきです。例えば、StackOverflowのLDAPの例にはFilterのエスケープDNのエスケープが全くない例が載っていたりします。APIを使っていれば大丈夫、と勘違いしているのかも知れません(?)紹介した例を安易にモデルに書いたりすると問題になります。RubyプログラマはPHPプログラマよりスキルが高い人が多いから、スニペットやライブラリも安全!と思っていると落とし穴に落ちてしまいます。本格的にプログラムを作っていないWebデザイナ系の方も多いPHPとは多少状況が異なりますが、幅広い開発者によって支えられている現在のRuby/Railsはそう変わらない、と思って使っていた方が安全です。

入力バリデーションは境界防御となるコントローラーでのバリデーションをお奨めしますが、モデルでのバリデーションでも十分という場合にはRails風にモデルでバリデーションでも構いません。1対1でコントローラーとモデルがマッピングされている、シンプルなアプリケーションなら大きな違いはありません。

バリデーションを自動で行なってくれる物はあまりありません。基本的に全て自分がバリデーションしなければならない、と考えてください。APIやライブラリを使う場合にも、エスケープが必要なのか?バリデーションが必要なのか?と確認してから使うと良いでしょう。(参考:エスケープファースト、この順序は変えられない

 

まとめ

まだまだ色々あるのですが、Ruby/Railsの事を書くより、PHPの事を書かなければならないので今日の所はこの辺りで終わりにしておきます。Ruby/Railsの事も書いて欲しい!という要望が多いようであれば、PHP本が一段落したら書きたいと思います。あまりRuby/Railsで開発している方はこのブログを見ていないと思うので、ニーズは多くないかな?と思っています。

ここに紹介した物も含まれていますが、少なくともRuby on Rails Security Guideを理解してから開発しましょう。Railsは自動的に安全なアプリケーションを作ってくれるプラットフォームではありません。Railsアプリの書き方は異なりますが、PHPフレームワークを使ったアプリとあまり変わらない、と思って使った方が良いです。

ところでRuby 2.1のscrub良いですね。基本的には壊れた文字エンコーディングのデータを処理するべきではありません(バリデーションするか例外で処理中止すべきです)が、どうしても処理する必要がある場合もあります。エンコーディング変換で文字列として不正なバイト列を除去する方法でも構わないのですが、APIがある事は重要だと思います。

ここに書いた方法より良い方法、ここは違うかな?という部分がある場合、コメント頂けるとありがたいです!

 

投稿者: yohgaki