ITコンサルの日常

ITコンサル会社に勤務する普通のITエンジニアの日常です。

「RailsによるアジャイルWebアプリケーション開発」26章読了

SQLインジェクション

定義済みメソッド(findなど)やバインド変数を使っていれば問題ないというお話しです。
試しに問題となるコードと、簡単な攻撃を行ってみたいと思います。


まずはコントローラ。
app/controllers/products_controller.rb

  def sql_injection
    unless params[:title].blank?
      @products = Product.find(:all, :conditions => "title = '#{params[:title]}'")
    else
      @products = []
    end
  end

パラメータのtitleが、データベースのtitle列と一致する行を取得するコントローラです。


次にビュー。
app/views/products/sql_injection.html.erb

<% form_tag :action => :sql_injection, :id => params[:id] do |f| %>
  Title: <%= text_field_tag "title" %><br>
  <%= submit_tag "push" %>
<% end %>
<ul>
  <% for product in @products %>
  <li><%= product.title %></li>
  <% end %>
</ul>

検索条件としてのtitleを入力するフォームと、検索結果をリスト表示する部分があります。


普通に検索してみたところ。

次に、検索条件に全件ヒットするような条件を付加して検索してみたところ。

見事に攻撃されちゃってます。
これを防ぐには、上記のようなコーディングはしてはいけないということを覚えておいた方が良さそうですね。

フォームパラメータを直接使ったレコードの作成

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

みたいなコードだと、フォームから受け取った値でレコードを作りに(INSERTしに)いきます。
仮に承認フラグとか特権区分とか、フォームから入力する以外の値で作る列があった場合に、捏造フォームからそれらの値を設定されてしまいますよ、という脅威に対する機能。


ちなみにscaffoldした状態だと、createアクションはこんな感じ。

  # POST /products
  # POST /products.xml
  def create
    @product = Product.new(params[:product])

    respond_to do |format|
      if @product.save
        flash[:notice] = 'Product was successfully created.'
        format.html { redirect_to(@product) }
        format.xml  { render :xml => @product, :status => :created, :location => @product }
      else
        format.html { render :action => "new" }
        format.xml  { render :xml => @product.errors, :status => :unprocessable_entity }
      end
    end
  end

受け取ったパラメータで@productインスタンスを作って、その下でsaveしているので、上のコードと変わりません。


実験として、descriptionは、フォームから受け取った値で作成しないとします。
この場合、モデルを変更します。
app/models/products.rb

class Product < ActiveRecord::Base
  attr_protected :description
end

ちなみにホワイトリスト方式(?)でも書くことが出来て、その場合はattr_accessibleで指定します。


試しに新しいproductを作ってみる。

その結果。

というわけで、descriptionのフィールドに入れたにも関わらず、
結果としては入っていません。

このフィールドに代入するには、明示的に、

product.description = params[:product][:description]

のように書く必要があります。

クロスサイトスクリプティング対策

XSSについて詳しくはこちら。
http://ja.wikipedia.org/wiki/%E3%82%AF%E3%83%AD%E3%82%B9%E3%82%B5%E3%82%A4%E3%83%88%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%86%E3%82%A3%E3%83%B3%E3%82%B0


XSSって、
入力中の一定のタグをHTMLとして反映したい場合(例: ウィキ)
だけかと思ってたら違うのですね。
認識が甘いなあ。


対策にある、HTMLの実体参照を用いうんぬんっていうのが、Railsでいうところのh(html_escape)メソッド。
scaffoldしたアプリケーションでは、デフォルトで入ってます。

app/views/products/show.html.erb

<p>
  <b>Title:</b>
  <%=h @product.title %>
</p>

<p>
  <b>Description:</b>
  <%=h @product.description %>
</p>

<p>
  <b>Image url:</b>
  <%=h @product.image_url %>
</p>


<%= link_to 'Edit', edit_product_path(@product) %> |
<%= link_to 'Back', products_path %>

この場合HTMLタグが単なるテキストとして表示されます。

次に、入力中の一定のタグをHTMLとして反映したい場合(例: ウィキ)の対策。
これは、Railsでいうところのsanitizeメソッドが相当します。
さっきのテンプレートで、descriptionをsanitizeするように変えてみる。

<p>
  <b>Title:</b>
  <%=h @product.title %>
</p>

<p>
  <b>Description:</b>
  <%=sanitize @product.description %>
</p>

<p>
  <b>Image url:</b>
  <%=h @product.image_url %>
</p>


<%= link_to 'Edit', edit_product_path(@product) %> |
<%= link_to 'Back', products_path %>

この場合、strongのような無害なタグは残り、scriptのような有害なタグは削除され(その他もろもろの無害化が行われ)ます。

ちなみにいずれの対策もしなかった場合。

スクリプトが実行されちゃってます。

セッション固定攻撃

http://bakera.jp/glossary/30bb30c330b730e730f356fa5b9a653b648
ということらしい。


脚注のURLは変わっていて、
http://www.windowsecurity.com/whitepapers/Session_Fixation_Vulnerability_in_Webbased_Applications.html
になっているようです。


ユーザがログインする度に新しいセッションを作成するというのが対策のようです。
たぶん、セッションに何か格納する前にreset_sessionすれば良いはず。


試しにこんなサンプルを作ってみた。
まずはいつも通りコントローラから。
app/controllers/products_controller.rb

  def session_test
    if params[:val]
      if params[:val] == "clear"
        reset_session
      else
        session[:val] = params[:val]
      end
    end
  end

パラメータvalの値が入っていて、"clear"の場合はセッションリセット。"clear"以外の場合はセッションに値を格納する。
パラメータvalの値が入っていない場合は、セッション操作は行わない。


で次にビュー。

Session[:val] = <%= session[:val] %>

<p>
<%= link_to "abc", :action => :session_test, :id => params[:id], :val => 'abc' %><br/>
<%= link_to "reload", :action => :session_test, :id => params[:id] %><br/>
<%= link_to "clear", :action => :session_test, :id => params[:id], :val => 'clea
r' %>

セッションの中身を表示する部分と、それぞれ

  • セッションにabcを設定
  • セッションは何もせず再描画するだけ
  • セッションをクリア

するリンクを設置しました。


まず、abcのリンクをクリックするとセッションが設定されて、データベースの中身はこんな感じ。

sqlite> select * from sessions;
1|cf9e9da7f832493fd869eaa20c60b66c|BAh7BzoIdmFsIghhYmMiCmZsYXNoSUM6J0FjdGlvbkNvbnRyb2xsZXI6OkZs
YXNoOjpGbGFzaEhhc2h7AAY6CkB1c2VkewA=
|2008-07-29 05:23:27|2008-07-29 05:23:27


次に、reloadのリンクをクリックするとセッションは変わらず、データベースの中身はこんな感じ。

sqlite> select * from sessions;
1|cf9e9da7f832493fd869eaa20c60b66c|BAh7BzoIdmFsIghhYmMiCmZsYXNoSUM6J0FjdGlvbkNvbnRyb2xsZXI6OkZs
YXNoOjpGbGFzaEhhc2h7AAY6CkB1c2VkewA=
|2008-07-29 05:23:27|2008-07-29 05:23:27


最後にclearのリンクをクリックしてから、再度abcのリンクをクリックすると、セッションが再設定されて、データベースの中身はこんな感じ。

sqlite> select * from sessions;
2|2d48ecb2ba1c3b0baa999ce8e95f29fb|BAh7BzoIdmFsIghhYmMiCmZsYXNoSUM6J0FjdGlvbkNvbnRyb2xsZXI6OkZs
YXNoOjpGbGFzaEhhc2h7AAY6CkB1c2VkewA=
|2008-07-29 05:23:54|2008-07-29 05:23:56

session_id(2列目)が変わってますね。


このことから、ログイン成功した後に、ユーザIDとかをセッションに格納する前にreset_session呼んであげれば、セッション固定攻撃は回避出来ると思います。

機密情報の転送にSSLを使う

開発環境でもSSLできるんかいなと思って調べてたら、
http://d.hatena.ne.jp/elm200/20070428/1177752908
のサイトが良さげでした。


ssl_requirementプラグインもなんとなく動いている感じだったが、
InvalidAuthenticityTokenとか出て、
なんだかまたRails2.x問題っぽかったので深追いしないことにする。