こんにちは。僕はこのたびセキュリティキャンプ九州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に以下を追記。
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.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を設定します。
root to: "home#index"
home/index.html.erbにログインと登録用のURLを追加します。
<h1>Home
<% 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の一部をコメントアウトします。
class User < ActiveRecord::Base
devise :database_authenticatable, :registrable,
:recoverable, :rememberable
end
その修正に合わせて (タイムスタンプ)__devise_create_users.rbの一部もコメントアウトします。
class DeviseCreateUsers < ActiveRecord::Migration
def change
create_table :users do |t|
t.string :email, null: false, default: ""
t.string :encrypted_password, null: false, default: ""
t.string :reset_password_token
t.datetime :reset_password_sent_at
t.datetime :remember_created_at
t.timestamps null: false
end
add_index :users, :email, unique: true
add_index :users, :reset_password_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にアクセスするとこうなります。

Sign upから登録をすませると

これで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に追記します。
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.email
を current_user.username
に変更します。
app/views/home/index.html.erb
以下のviewの :email
を :username
に変更し、 email_field
を text_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にアクセスすると

usernameも登録できるようになっていて

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属性も設定します。
has_many :posts, dependent: :destroy
以下のコマンドを実行します。
% bundle exec rake db:migrate
== 20160920092635 CreatePosts: migrating ======================================
-- create_table(:posts)
-> 0.0017s
== 20160920092635 CreatePosts: migrated (0.0017s) =============================
投稿と投稿者が結びつくようにフォームを修正します。
<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に追記してください。
scope :by_body_like, ->(body){
where("body LIKE '%"+body+"%'")
}
scope :by_users_id, ->(users_id){
where(user_id: users_id)
}
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に追記します。
POST_SEARCH = 1
USER_SEARCH = 2
この定数は Post::POST_SEARCH
と記述すると使えます。
次にcontrollerを記述します。
今回の検索機能はsearch_typeで検索する対象(postのbody,userのusername)を決定し、searchが検索ワードという実装にしました。
posts_controller.rbのindexメソッドを次のように変更します。
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に追記します。
<%= 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の完成です。

次は脆弱性検証編です。
euglena1215.hatenablog.jp
ソースはこちら
GitHub - euglena1215/testApp