カレーの恩返し

おいしいのでオススメ。

Railsでセキュリティのことを考えずに作ったwebアプリは脆弱性があるのか調べてみた(webアプリ作成編)

こんにちは。僕はこのたびセキュリティキャンプ九州2016に行ってきました。
一言感想を言わせていただくと大変面白かったです。
ただ一つ不安になったことがあります。それはwebアプリの脆弱性です。 キャンプ中にwebアプリの脆弱性を見つけてみようという講座があり、脆弱性があるwebアプリを公開してみんなで脆弱性を探すというものだったのですが、30分もたたないうちに実装されてないはずの画像アップロード機能が追加されたりページを開いた瞬間音声が流れ出す迷惑サイトに変化していたりと蜂の巣状態になっていました。

そこで今回は普段使っているRuby on Railsで作ったwebアプリに脆弱性が存在するのかを確かめてみようと思います。

仕様

セキュリティキャンプ九州で使用した脆弱SNSの機能は次のようなものでした。

  • ログイン機能(登録・編集)
  • ログインすると掲示板への投稿・削除ができる
  • 投稿内容の一覧
  • 投稿とユーザの検索機能

DB

  • 投稿テーブル(内容/投稿者/日時)
  • ユーザテーブル(ユーザ名/パスワード/URL)

これと同じような機能のwebアプリをRailsでできるだけコードを書かずに作ってみたいと思います。
Railsではログイン機能はコマンドだけでは生成できないので超有名なユーザー管理のgemであるDeviseを使いました。

環境

Rails 4.2.4
Devise 4.1.1

ログイン機能

まずはrailsアプリを生成。 以下のコマンドを実行します。

% rails new testApp
      create
      create  README.rdoc
      create  Rakefile
      create  config.ru
      create  .gitignore
      create  Gemfile
      create  app
      create  app/assets/javascripts/application.js
      create  app/assets/stylesheets/application.css
      create  app/controllers/application_controller.rb
      create  app/helpers/application_helper.rb
      create  app/views/layouts/application.html.erb
  — 略 —
      create  vendor/assets/stylesheets/.keep
         run  bundle install
Fetching gem metadata from https://rubygems.org/
Fetching version metadata from https://rubygems.org/
Fetching dependency metadata from https://rubygems.org/
Resolving dependencies......
Using rake 11.2.2
Using i18n 0.7.0
Using json 1.8.3
— 略 —

Gemfileに以下を追記。

# Gemfile
gem ‘devise’ 

bundle installを実行。
% bundle install

rails generateでdeviseをインストールします。
これでユーザー管理を行うための準備が完了します。

% bundle exec rails generate devise:install
Running via Spring preloader in process 4640
      create  config/initializers/devise.rb
      create  config/locales/devise.en.yml
===============================================================================

Some setup you must do manually if you haven't yet:

  1. Ensure you have defined default url options in your environments files. Here
     is an example of default_url_options appropriate for a development environment
     in config/environments/development.rb:

       config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }

     In production, :host should be set to the actual host of your application.

  2. Ensure you have defined root_url to *something* in your config/routes.rb.
     For example:

       root to: "home#index"

  3. Ensure you have flash messages in app/views/layouts/application.html.erb.
     For example:

       <p class="notice"><%= notice %></p>
       <p class="alert"><%= alert %></p>

  4. If you are deploying on Heroku with Rails 3.2 only, you may want to set:

       config.assets.initialize_on_precompile = false

     On config/application.rb forcing your application to not access the DB
     or load models when precompiling your assets.

  5. You can copy Devise views (for customization) to your app by running:

       rails g devise:views

===============================================================================

言われた通りに設定していきます。
development.rbに追記します。

# config/enviroments/development.rb  
config.action_mailer.default_url_options = { :host => 'localhost:3000' }
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
    :address => 'smtp.gmail.com',
    :port => 587,
    :authentication => :plain,
    :user_name => 'メールアドレス',
    :password => 'パスワード'
}

root用のcontrollerとviewをコマンドで生成します。

% bundle exec rails generate controller home index
Running via Spring preloader in process 7935
      create  app/controllers/home_controller.rb
       route  get 'home/index'
      invoke  erb
      create    app/views/home
      create    app/views/home/index.html.erb
      invoke  test_unit
      create    test/controllers/home_controller_test.rb
      invoke  helper
      create    app/helpers/home_helper.rb
      invoke    test_unit
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/home.coffee
      invoke    scss
      create      app/assets/stylesheets/home.scss

routes.rbに追記してroot_urlを設定します。

# config/routes.rb
root to: "home#index"

home/index.html.erbにログインと登録用のURLを追加します。

<h1>Home#index</h1>
<% if user_signed_in? %>
  Logged in as <strong><%= current_user.email %></strong>.
  <%= link_to "Settings", edit_user_registration_path, :class => "navbar-link" %> |
  <%= link_to "Logout", destroy_user_session_path, method: :delete, :class => "navbar-link" %>
<% else %>
  <%= link_to "Sign up", new_user_registration_path, :class => 'navbar-link' %> |
  <%= link_to "Login", new_user_session_path, :class => 'navbar-link' %>
<% end %>
<p>Find me in app/views/home/index.html.erb</p>

ログイン情報を出力させるためにlayouts/application.html.erbに追加します。

<!DOCTYPE html>
<html>
<head>
  <title>TestApp</title>
  <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track' => true %>
  <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
  <%= csrf_meta_tags %>
</head>
<body>
<% if notice %>
  <p class="alert alert-success"><%= notice %></p>
<% end %>
<% if alert %>
  <p class="alert alert-danger"><%= alert %></p>
<% end %>
<%= yield %>

</body>
</html>

次にユーザー用のmodelとmigrationファイルを生成します。

% bundle exec rails generate devise User
Running via Spring preloader in process 4790
      invoke  active_record
      create    db/migrate/(タイムスタンプ)_devise_create_users.rb
      create    app/models/user.rb
      invoke    test_unit
      create      test/models/user_test.rb
      create      test/fixtures/users.yml
      insert    app/models/user.rb
       route  devise_for :users

今回はtrackableとvalidatableは使わないので user.rbの一部をコメントアウトします。

# app/models/user.rb
class User < ActiveRecord::Base
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registrable,
         :recoverable, :rememberable
        # :trackable,
        # :validatable
end

その修正に合わせて (タイムスタンプ)__devise_create_users.rbの一部もコメントアウトします。

# db/migrate/(タイムスタンプ)__devise_create_users.rb
class DeviseCreateUsers < ActiveRecord::Migration
  def change
    create_table :users do |t|
      ## Database authenticatable
      t.string :email,              null: false, default: ""
      t.string :encrypted_password, null: false, default: ""

      ## Recoverable
      t.string   :reset_password_token
      t.datetime :reset_password_sent_at

      ## Rememberable
      t.datetime :remember_created_at

      ## Trackable
      # t.integer  :sign_in_count, default: 0, null: false
      # t.datetime :current_sign_in_at
      # t.datetime :last_sign_in_at
      # t.string   :current_sign_in_ip
      # t.string   :last_sign_in_ip

      ## Confirmable
      # t.string   :confirmation_token
      # t.datetime :confirmed_at
      # t.datetime :confirmation_sent_at
      # t.string   :unconfirmed_email # Only if using reconfirmable

      ## Lockable
      # t.integer  :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts
      # t.string   :unlock_token # Only if unlock strategy is :email or :both
      # t.datetime :locked_at


      t.timestamps null: false
    end

    add_index :users, :email,                unique: true
    add_index :users, :reset_password_token, unique: true
    # add_index :users, :confirmation_token,   unique: true
    # add_index :users, :unlock_token,         unique: true
  end
end

DBを生成します。以下のコマンドを実行してください。

% bundle exec rake db:migrate
== 20160920061425 DeviseCreateUsers: migrating ================================
-- create_table(:users)
   -> 0.0012s
-- add_index(:users, :email, {:unique=>true})
   -> 0.0007s
-- add_index(:users, :reset_password_token, {:unique=>true})
   -> 0.0008s
== 20160920061425 DeviseCreateUsers: migrated (0.0029s) =======================

rails s でサーバを起動させ、http://localhost:3000にアクセスするとこうなります。 f:id:euglena1215:20160921160404p:plain

Sign upから登録をすませると f:id:euglena1215:20160921160515p:plain

これでdeviseのデフォルトのログイン機能は完了ですが、現在ではユーザ情報がメールアドレスとパスワードしか存在しません。 なのでユーザ情報にユーザ名を追加し、ユーザ名とパスワードでログインできるようにします。

以下のコマンドを実行します。

% rails generate migration add_username_to_users username:string
Running via Spring preloader in process 9875
      invoke  active_record
      create    db/migrate/(タイムスタンプ)_add_username_to_users.rb

usernameはログインで使われるためuniqueを与えます。 (タイムスタンプ)_add_username_to_users.rbに追記します。

#db/migrate/(タイムスタンプ)_add_username_to_users.rb
class AddUsernameToUsers < ActiveRecord::Migration
  def change
    add_column :users, :username, :string
    add_index :users, :username, unique: true
  end
end

以下のコマンドを実行します。

% bundle exec rake db:migrate
== 20160920080558 AddUsernameToUsers: migrating ===============================
-- add_column(:users, :username, :string)
   -> 0.0018s
-- add_index(:users, :username, {:unique=>true})
   -> 0.0025s
== 20160920080558 AddUsernameToUsers: migrated (0.0045s) ======================

user.rbにvalidationを追加します。

validates :username, presence: true, uniqueness: true

ログインで使うデータを変更したため、viewをカスタマイズします。

以下のコマンドを実行します。

% rails g devise:views
Running via Spring preloader in process 11412
      invoke  Devise::Generators::SharedViewsGenerator
      create    app/views/devise/shared
      create    app/views/devise/shared/_links.html.erb
      invoke  form_for
      create    app/views/devise/confirmations
      create    app/views/devise/confirmations/new.html.erb
      create    app/views/devise/passwords
      create    app/views/devise/passwords/edit.html.erb
      create    app/views/devise/passwords/new.html.erb
      create    app/views/devise/registrations
      create    app/views/devise/registrations/edit.html.erb
      create    app/views/devise/registrations/new.html.erb
      create    app/views/devise/sessions
      create    app/views/devise/sessions/new.html.erb
      create    app/views/devise/unlocks
      create    app/views/devise/unlocks/new.html.erb
      invoke  erb
      create    app/views/devise/mailer
      create    app/views/devise/mailer/confirmation_instructions.html.erb
      create    app/views/devise/mailer/password_change.html.erb
      create    app/views/devise/mailer/reset_password_instructions.html.erb
      create    app/views/devise/mailer/unlock_instructions.html.erb

以下のviewの current_user.emailcurrent_user.username に変更します。
app/views/home/index.html.erb

以下のviewの :email:username に変更し、 email_fieldtext_field に変更します。
app/views/devise/sessions/new.html.erb

以下のviewにusername用のフォームを追加します。
app/views/devise/registrations/new.html.erb
app/views/devise/registrations/edit.html.erb

先ほど登録したuserはusernameにデータが入っていないので一度DBをリセットします。
以下のコマンドを実行します。

% bundle exec rake db:migrate:reset
== 20160920061425 DeviseCreateUsers: migrating ================================
-- create_table(:users)
   -> 0.0013s
-- add_index(:users, :email, {:unique=>true})
   -> 0.0007s
-- add_index(:users, :reset_password_token, {:unique=>true})
   -> 0.0007s
== 20160920061425 DeviseCreateUsers: migrated (0.0029s) =======================

== 20160920080558 AddUsernameToUsers: migrating ===============================
-- add_column(:users, :username, :string)
   -> 0.0006s
-- add_index(:users, :username, {:unique=>true})
   -> 0.0011s
== 20160920080558 AddUsernameToUsers: migrated (0.0018s) ======================

rails s でサーバを起動し http://localhost:3000/users/sign_upにアクセスすると f:id:euglena1215:20160921160700p:plain

usernameも登録できるようになっていて f:id:euglena1215:20160921160719p:plain

usernameとpasswordでログインできるようになっています。

次にURLカラムを追加します。 以下のコマンドを実行します。

% rails generate migration add_url_to_users url:string
Running via Spring preloader in process 12882
      invoke  active_record
      create    db/migrate/20160920090632_add_url_to_users.rb

先ほどと同様に以下のviewにurl用のフォームを追加します。
app/views/devise/registrations/new.html.erb
app/views/devise/registrations/edit.html.erb

先ほど登録したuserはurlを持っていないので一度DBをリセットします。
以下のコマンドを実行します。

% bundle exec rake db:migrate:reset
== 20160920061425 DeviseCreateUsers: migrating ================================
-- create_table(:users)
   -> 0.0012s
-- add_index(:users, :email, {:unique=>true})
   -> 0.0006s
-- add_index(:users, :reset_password_token, {:unique=>true})
   -> 0.0007s
== 20160920061425 DeviseCreateUsers: migrated (0.0028s) =======================

== 20160920080558 AddUsernameToUsers: migrating ===============================
-- add_column(:users, :username, :string)
   -> 0.0006s
-- add_index(:users, :username, {:unique=>true})
   -> 0.0010s
== 20160920080558 AddUsernameToUsers: migrated (0.0017s) ======================

== 20160920090632 AddUrlToUsers: migrating ====================================
-- add_column(:users, :url, :string)
   -> 0.0005s
== 20160920090632 AddUrlToUsers: migrated (0.0006s) ===========================

これでログイン機能はOKです。

投稿機能

以下のコマンドを実行します。

% rails g scaffold post body:text user:references
Running via Spring preloader in process 13853
      invoke  active_record
      create    db/migrate/20160920092635_create_posts.rb
      create    app/models/post.rb
      invoke    test_unit
      create      test/models/post_test.rb
      create      test/fixtures/posts.yml
      invoke  resource_route
       route    resources :posts
      invoke  scaffold_controller
      create    app/controllers/posts_controller.rb
      invoke    erb
      create      app/views/posts
      create      app/views/posts/index.html.erb
      create      app/views/posts/edit.html.erb
      create      app/views/posts/show.html.erb
      create      app/views/posts/new.html.erb
      create      app/views/posts/_form.html.erb
      invoke    test_unit
      create      test/controllers/posts_controller_test.rb
      invoke    helper
      create      app/helpers/posts_helper.rb
      invoke      test_unit
      invoke    jbuilder
      create      app/views/posts/index.json.jbuilder
      create      app/views/posts/show.json.jbuilder
      create      app/views/posts/_post.json.jbuilder
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/posts.coffee
      invoke    scss
      create      app/assets/stylesheets/posts.scss
      invoke  scss
      create    app/assets/stylesheets/scaffolds.scss

ユーザと投稿は1対多の関係なのでrelationをuser.rbに追記します。
また、ユーザが削除されたときにそのユーザの投稿も削除してほしいのでdependent属性も設定します。

# app/models/user.rb
has_many :posts, dependent: :destroy

以下のコマンドを実行します。

% bundle exec rake db:migrate
== 20160920092635 CreatePosts: migrating ======================================
-- create_table(:posts)
   -> 0.0017s
== 20160920092635 CreatePosts: migrated (0.0017s) =============================

投稿と投稿者が結びつくようにフォームを修正します。

# app/views/posts/_form.html.erb
<div class="field">
  <%= f.label :body %><br>
  <%= f.text_area :body %>
  <%= f.hidden_field :user_id, value: current_user.id %>
</div>

まだ色々な微調整は済んでいませんが、基本的な投稿機能は完成です。 (書くのが面倒くさくなりました。誠に申し訳ございません。一番下にソースのリンクを貼っておくので参考にしてください)

検索機能

次に検索機能を実装します。 今後も使っていくようなアプリであれば検索機能用のgemであるransackを使うのですが、今回は簡単な検索のみなのでgemを使わずに実装しようと思います。

まずはscopeを実装します。post.rbとuser.rbに追記してください。

# app/models/post.rb
scope :by_body_like, ->(body){
  where("body LIKE '%"+body+"%'")
}

scope :by_users_id, ->(users_id){
  where(user_id: users_id)
}
# app/models/user.rb
scope :by_username_like, ->(username){
  where("username LIKE '%"+username+"%'")
}

これで Post.by_body_like(‘hoge’) で投稿内容に’home'が含まれる投稿を取得することができます。
また、 Post.by_users_id(User.by_username_like(‘foo’).pluck[:id]) でユーザー名に’foo’が含まれるユーザーの投稿を取得することができます。
注意!! この記述には脆弱性が存在します。詳しくは脆弱性検証編を書いてあります。

次は検索機能で用いるラジオボタン用の定数を設定します。 post.rbに追記します。

# app/models/post.rb
POST_SEARCH = 1
USER_SEARCH = 2

この定数は Post::POST_SEARCH と記述すると使えます。

次にcontrollerを記述します。
今回の検索機能はsearch_typeで検索する対象(postのbody,userのusername)を決定し、searchが検索ワードという実装にしました。
posts_controller.rbのindexメソッドを次のように変更します。

# app/controllers/posts_controller.rb
def index
  if params[:search_type] == Post::POST_SEARCH.to_s
    @posts = Post.by_body_like(params[:search])
  elsif params[:search_type] == Post::USER_SEARCH.to_s
    users_id = User.by_username_like(params[:search]).pluck(:id)
    @posts = Post.by_users_id(users_id)
  else
    @posts = Post.all
  end
end

Post::POST_SEARCH.to_sとなっているのはGETメソッドから得られるパラメータは数値も文字列に変換されているためです。

次にviewを記述します。
posts/index.html.erbに追記します。

# app/views/posts/index.html.erb
<%= form_tag({controller: '/posts',action: 'index'}, method: 'get', class: 'search_form', style: 'padding: 20px;') do %>
  <div class="field">
    <%= radio_button_tag :search_type, Post::POST_SEARCH %>
    <%= label_tag :post %>
    <%= radio_button_tag :search_type, Post::USER_SEARCH %>
    <%= label_tag :user %>
  </div>
  <div class="field">
    <%= text_field_tag :search,'' %>
  </div>
  <div class="actions">
    <%= submit_tag '検索' %>
  </div>
<% end %>

これで検索機能は完成です。

ここから

  • 新規投稿機能をトップページに持ってくる
  • bootstrapをCDNで読み込ませる
  • UIの調整
  • 使ってないメソッド、ルーティング、ビューを削除
  • タイムゾーンを東京に変更

などなどを書き換えると脆弱かもしれないSNSの完成です。 f:id:euglena1215:20160921162147p:plain

次は脆弱性検証編です。 euglena1215.hatenablog.jp

ソースはこちら GitHub - euglena1215/testApp