AWDwR "Chapter21 Securing Your Rails Application"

2005年もあと5日。Chapter21はセキュリティについて。

SQL Injection

フォームなど外部からのデータを直接SQL文の中で使ったりすると悪い人に任意のSQL文が実行されちゃうよというのがSQLインジェクション。例えばこんな風にしていると危ない。

Email.find(:all, :conditions => "owner_id = 123 and subject = '#{params[:subject]}'")

この場合もし悪い人がサブジェクトに' or 1 -- 'と入力しちゃうと全データが表示されちゃう。

このSQLインジェクションを防ぐためにはSQLメタキャラクタの\や'をちゃんとエスケープしましょう。Active Recordのattributes(), save(), find()で条件やリミットやSQL文を追加しない場合はActive Recordが危険な文字をエスケープしてくれるから大丈夫。しかし条件やリミットやSQL文を含めた場合は外部からのデータにSQLメタキャラクタが含まれないように気をつけること。そのためにはSQLステートメントの一部に#{...}を使うのではなくバインド変数を使うようにしましょう。

Email.find(:all, :conditions => ["ownerid = 123 and subject = ?", subject])

また?ではなくハッシュを渡す方法もあります。

Order.find(:all. :conditions => ["name = :name and type = :type",
                                {:name => name, :type => type}])

それからRailsが自動的に作るメソッドもSQLインジェクションに対しては安心。

Email.find_all_by_owner_id_and_subject(owner.id, subject)

Cross-Site Scripting (CSS/XSS)

外部からのデータをそのまま表示しちゃうと、例えばJavaScriptを使ってセッションIDが盗まれログインされてしまうよというのがクロスサイトスクリプティング

クロスサイトスクリプティングを防ぐにはHTMLメタキャラクタの<と>をHTMLエンティティの&lt;と&gt;に変換してしまえばOK。RailsはHTMLメタキャラクタをエスケープするh()メソッド(html_escape()のエリアス)があるのでそれを使いましょう。とにかくビューでは全ての変数に対してh()メソッドを使うこと。

また、テンプレートの中でHTMLを含んだ文字列に置き換えたい場合はsanitize()というメソッドを使いましょう。

Avoid Session Fixation Attacks

悪い人が入手したセッションIDをユーザに使わせることによって不正にログインしてしまうのがセッションフィクセーション攻撃。セッションフィクセーション攻撃を防ぐ方法には2つあって、一つがセッションを生成したときのIPアドレスを覚えておいて、それが変わったらセッションをキャンセルしてしまうようにするというもの。もう一つは誰かがログインするごとに新しいセッションを生成するようにしてしまえばよい。

Creating Records Directly from Form Parameters

ユーザを登録するアクションはよく以下のようにするんだけど

def register
  User.create(params[:user])
end

もし悪い人が以下のようなフォームを作ってデータを送信しちゃうと

<form method="post" action="http://www.foo.com/user/register">
  <input type="text" name="user[name]">
  <input type="text" name="user[password]">
  <input type="text" name="user[role]"> # ロールは本来なら管理者のみが設定できるもの
</form>

ロールを任意に設定されてしまうことになってしまう。

Active Recordはこれを防ぐ方法を2つ持っていて、1つがattr_protected()メソッドで保護したい属性を指定する方法。指定された属性はcreate()やnew()でparamsからモデルにいっぺんに設定することはできなくなる。この属性に値を設定したい場合は以下のように直接指定するようにすればよい。

user.role = params[:user][:role]

逆にattr_accessible()メソッドで設定してよい属性を指定する方法もある。この場合はそれ以外の属性は保護されるようになる。

Don't Trust ID Parameters

find()でIDだけで他の条件を指定せずにDBからデータを取り出すようにすると、悪い人がIDを推定して入力することにより任意のデータを取り出すことができてしまう。これを防ぐには条件付のfind()を使うようにしましょう。例えば以下の例ではユーザIDがログインユーザのものだけという条件をつけている。

def show
  id = params[:id]
  user_id = session[:user_id] || -1
  @order = Order.find(id, :conditions => ["user}id = ?", user_id])
end

delete()やdestroy()も同じ問題があって、さらにこれらには:conditionsパラメタがないのでデータのオーナをチェックするとかwhere節を指定するとか自分でちゃんと確認しないとダメですよ。

他の方法としては関連を使うものがあって、これはuser has_many ordersと指定していれば以下のようにできる。

user.orders.find(:params[:id])

Don't Expose Controller Methods

コントローラ内のパブリックメソッドであるアクションはユーザに直接実行される場合があるので、そのこともよーく考えてアクセス権限等が守られるように注意すること。例えば以下のようにオーナのユーザIDを確認する。

def read
  @email = Email.find(params[:id])
  unless @email.owner_id == session[:user_id]
    flash[:notice] = "Not Found"
    redirect_to(:action => 'index')
  end
end

File Uploads

ユーザにアップロードさせたファイルをユーザにダウンロードさせる場合、そのファイルが.rhtmlや.cgi等の実行形式のファイルだったりすると、ダウンロード時に実行され任意のコードを実行される恐れがある。これを防ぐ方法としては、まずアップロードされたファイルを直接アクセスできるディレクトリに置かないようにし、アクションでそれらのファイルを表示するようにする。その際、注意する点としては

  • 指定されたファイル名が存在するファイル名またはDBに登録されているファイル名かどうかを確認すること。その際../../etc/passwdのようなファイル名は許可しないようにする。
  • ダウンロードしたファイルをブラウザで表示させる場合はHTMLシーケンスをエスケープすること。またバイナリデータをダウンロードさせる場合はブラウザで表示しないようにContent-Typeに適当なタイプを設定すること。

Don't Cache Authenticated Pages

ページキャッシングはセキュリティフィルタをパスしてしまうので、セッション情報でアクセス制御を行ないたい場合は、アクションキャッシングまたはフラグメントキャッシングを使いましょう。

Knowing That It Works

コードを安全にしたい場合はテストを書きましょう。攻撃をシミュレートするためにRailsの機能テストを使いましょう。さらに見つかったセキュリティホールが修正されているかどうかを確認するためにも機能テストを使いましょう。同時にテストはあなたが考えたことだけを確認できるということを認識しましょう。


あと1章!