Slide 1

Slide 1 text

Railsアプリの脆弱性パターン iCARE Dev Meetup #14 2020/10/21 1

Slide 2

Slide 2 text

自己紹介 • 名前: 神速 • 会社: メドピア株式会社 • 所属: CTO室SRE • GitHub: @sinsoku (画像右上) • Twitter: @sinsoku_listy (画像右下) • Rails歴: 8年くらい 2

Slide 3

Slide 3 text

著書の同人誌1 1 表紙のイラスト: Ixyさん 3

Slide 4

Slide 4 text

9月の流行 • ドコモ口座を悪用した不正送金についてまとめてみた2 • 【お詫び】IPアドレスが他者からも確認できてしまう不具合に ついて3 3 https://note.jp/n/n3e6451c9b147 2 https://piyolog.hatenadiary.jp/entry/2020/09/08/054431 4

Slide 5

Slide 5 text

Railsアプリ x セキュリティ 5

Slide 6

Slide 6 text

15分では全てを話せない • セッションハイジャック • CSRF, XSS, CSP • オープンリダイレクト • SQLインジェクション • OSコマンドインジェクショ ン • Dos攻撃, DDos攻撃 • ブルートフォース • IPスプーフィング • タイミング攻撃 • DNSリバインディング • SSRF攻撃 6

Slide 7

Slide 7 text

何を話すか... 7

Slide 8

Slide 8 text

Railsセキュリティガイド4 4 https://railsguides.jp/security.html 8

Slide 9

Slide 9 text

9

Slide 10

Slide 10 text

徳丸浩さん「Railsエンジニアのためのウェブセキュリティ入門」5 5 https://www.slideshare.net/ockeghem/ruby-on-rails-security-142250872 10

Slide 11

Slide 11 text

セキュリティ専門家ではない 11

Slide 12

Slide 12 text

今日はなすこと 1. 脆弱なコード • ! 未検証なparamsの使用 • ! 個人情報(IPアドレスなど)の流出 2. 脆弱な仕様 • ! 認証に関わる仕様の事例 • ! 未ログインで使えるフォーム 12

Slide 13

Slide 13 text

! 未検証なparamsの使用 13

Slide 14

Slide 14 text

例: 記事にタグをつける機能 • 自分の記事にタグをつける 14

Slide 15

Slide 15 text

テーブルの設計6 # db/migrate/xxx_create_tags.rb def change create_table :tags do t.references :article, comment: "هࣄID" t.string :name, comment: "λά໊" end end 6 本当はタグと関連テーブルを別にした方が良い。 15

Slide 16

Slide 16 text

コントローラー # POST /tags def create @tag = Tag.new(tag_params) if @tag.save flash[:notice] = "λάΛ࡞੒͠·ͨ͠ɻ" else flash[:notice] = "λάͷ࡞੒ʹࣦഊ͠·ͨ͠ɻ" end redirect_to article_path(@tag.article) end def tag_params params.require(:tag).permit(:article_id, :name) end 16

Slide 17

Slide 17 text

画面のイメージ 17

Slide 18

Slide 18 text

記事の詳細ページ # GET /articles/:id def show @article = current_user.articles.find(params[:id]) @tag = Tag.new(article: @article) end ビューのコード例 = form_with @tag, local: true do |f| = f.hidden_field :article_id = f.text_field :name = f.submit 18

Slide 19

Slide 19 text

Chromeの開発者ツールで書き換え 19

Slide 20

Slide 20 text

他人の記事にタグをつけられる # POST /tags def create @tag = Tag.new(tag_params) if @tag.save flash[:notice] = "λάΛ࡞੒͠·ͨ͠ɻ" else flash[:notice] = "λάͷ࡞੒ʹࣦഊ͠·ͨ͠ɻ" end redirect_to article_path(@tag.article) end def tag_params params.require(:tag).permit(:article_id, :name) end 20

Slide 21

Slide 21 text

この問題の直し方 • パラメータを検証する • ルーティング設計を見直す 21

Slide 22

Slide 22 text

修正案1. パラメータを検証する # POST /tags def create # ࣗ෼ͷهࣄҎ֎͸404ʹ͢Δɻ unless current_user.articles.exists?(id: tag_params[:article_id]) raise ActiveRecord::NotFound end @tag = Tag.new(tag_params) # ུ end def tag_params params.require(:tag).permit(:article_id, :name) end 22

Slide 23

Slide 23 text

修正案2. ルーティング設計を見直す # POST /articles/:article_id/tags def create @article = current_user.articles.find(params[:article_id]) @tag = @article.tags.new(tag_params) # ུ end def tag_params params.require(:tag).permit(:name) end 23

Slide 24

Slide 24 text

Administrateの脆弱性の事例8 8 https://github.com/thoughtbot/administrate/commit/3ab838b83c5f565fba50e0c6f66fe4517f98eed3 24

Slide 25

Slide 25 text

directionを使った攻撃 GET /admin/users?direction="asc,10" で以下のようなSQLを実 行できる。 SELECT * FROM users ORDER BY users.created_at asc,10 カラム数より大きい値だとエラーになるため、攻撃者はテーブル のカラム数を特定できる。 25

Slide 26

Slide 26 text

! 個人情報(IPアドレスなど)の流出 26

Slide 27

Slide 27 text

noteのIP流出事件の概要 • フロントエンドはVue.js, Nuxt.js • APIで記事情報のJSONを返す • 著者の情報を含む • last_sign_in_ip も含まれていた • Deviseを使っているかは真偽不明 27

Slide 28

Slide 28 text

情報流出しそうなコード 記事情報を返すAPIを実装したぞ! Railsなら2行で実装できて便利!! # GET /api/v1/articles/:id def show article = Article.find(params[:id]) render json: article.as_json(include: :user) # { # "id": 1, "title": "foo", ..., # "user": { "id: 1, "name": "bar", ... }, # } end 28

Slide 29

Slide 29 text

(1年後...) ! 新しいアクセス元のときに警告を出そう 29

Slide 30

Slide 30 text

アクセス元IPをDBに保存しよう # db/migrate/xxxx_add_last_sign_in_ip_to_users.rb def change add_column :users, :last_sign_in_ip, :string end 30

Slide 31

Slide 31 text

情報流出するコード # GET /api/v1/articles/:id def show article = Article.find(params[:id]) render json: article.as_json(include: :user) end JSONに新しく追加したカラムが含まれてしまう。 31

Slide 32

Slide 32 text

修正する方法の例 # GET /api/v1/users/:id ARTICLE_ATTRIBUTES = %i[id title ...] USER_ATTRIBUTES = %i[id name ...] def show article = Article.find(params[:id]) json = article.as_json( only: ARTICLE_ATTRIBUTES, include: { user: { only: USER_ATTRIBUTES } } ) render json: json end 32

Slide 33

Slide 33 text

jb9を使った例 # app/views/articles/show.json.jb user = @article.user { id: @article.id, title: @article.title, ..., user: { id: user.id, name: user.name, ..., } } 9 https://github.com/amatsuda/jb 33

Slide 34

Slide 34 text

テストを書いておくと良い before { get "/api/v1/articles/#{article.id}" } it "ݸਓ৘ใΛؚ·ͳ͍ΧϥϜͷΈΛJSONͰฦ͢͜ͱ" do json = JSON.parse(response.body) expect(json).to contain_exactly("id", "title", ...) end 最近だとOpenAPI + committee10 で検証するのが良さそう。 10 https://github.com/interagent/committee 34

Slide 35

Slide 35 text

関連して... 35

Slide 36

Slide 36 text

秘匿情報をログに書き込まない Railsには秘匿値を[FILTERED]にする機能がある。 Started POST "/users" for ::1 at 2020-10-21 16:34:52 +0900 Processing by UsersController#create as HTML Parameters: {"authenticity_token"=>"6pxi6...g==", \ "user"=>{"name"=>"sinsoku", "password"=>"[FILTERED]"}, "commit"=>"Create User"} ↳ app/controllers/users_controller.rb:30:in `block in create' ↳ app/controllers/users_controller.rb:30:in `block in create' ↳ app/controllers/users_controller.rb:30:in `block in create' Redirected to http://localhost:3000/users/1 Completed 302 Found in 9ms (ActiveRecord: 2.1ms | Allocations: 2968) 36

Slide 37

Slide 37 text

ログに個人情報を書き込まない # config/initializers/filter_parameter_logging.rb # https://github.com/rails/rails/pull/34218 ͷ௥Ճ෼Λ൓өࡁΈɻ Rails.application.config.filter_parameters += [ :password, :secret, :token, :_key, :auth, :crypt, :salt, :certificate, :otp, :access, :private, :protected, :ssn ] 37

Slide 38

Slide 38 text

外部サービスに個人情報を送らない # config/initializers/sentry.rb Raven.configure do |config| config.sanitize_fields = Rails.application.config.filter_parameters.map(&:to_s) config.sanitize_http_headers = ["Via", "Referer", "User-Agent", "Server", "From"] end # config/initializers/rollbar.rb Rollbar.configure do |config| config.scrub_fields |= Rails.application.config.filter_parameters config.scrub_headers |= ["X-Access-Token"] end 38

Slide 39

Slide 39 text

! 認証に関わる仕様の事例 39

Slide 40

Slide 40 text

ログイン失敗時のエラーメッセージ 40

Slide 41

Slide 41 text

メールアドレスの存在確認ができる 41

Slide 42

Slide 42 text

メールの存在確認による攻撃 • フィッシングメールの準備 • メールアドレスの持ち主の嗜好が分かる • ブルートフォース攻撃の準備 • パスワードスプレー攻撃の準備 42

Slide 43

Slide 43 text

修正方法 43

Slide 44

Slide 44 text

ブルートフォース攻撃11 11 辞書攻撃 44

Slide 45

Slide 45 text

ブルートフォース攻撃の対策 • ! 連続でログイン失敗したIPアドレスを弾く • IPアドレスは変えられるので効果は薄い • " 簡単なパスワードを禁止する • # ログインに連続で失敗したらアカウントをロックする 45

Slide 46

Slide 46 text

redis-object12を使った認証失敗のカウント class User has_secure_password counter :login_fails, expiration: 1.day def authenticate(value) super.tap do |user_or_false| user_or_false ? login_fails.clear : login_fails.increment end end end 12 https://github.com/nateware/redis-objects 46

Slide 47

Slide 47 text

パスワードスプレー攻撃 47

Slide 48

Slide 48 text

パスワードスプレー攻撃の対策 • ! アカウントロックができない • 各アカウントで1回ずつしか失敗しない • " 簡単なパスワードを禁止する13 • # 多要素認証 13 1 Passwordの愛好者なので64文字まで許可して欲しい 48

Slide 49

Slide 49 text

! 未ログインで使えるフォーム 49

Slide 50

Slide 50 text

お問い合わせフォームを使った攻撃の事例14 14 https://www.security-next.com/115212 50

Slide 51

Slide 51 text

Slackがスパムで埋まる事例 51

Slide 52

Slide 52 text

対策: ハニーポット 不可視のフィールドを用意すると、botは入力してくる。 def create head :ok if contact_params[:pot].present? contact = Contact.new(contact_params) # ...ུ 52

Slide 53

Slide 53 text

今日はなしたこと 1. 脆弱なコード • ! 未検証なparamsの使用 • ! 個人情報(IPアドレスなど)の流出 2. 脆弱な仕様 • ! 認証に関わる仕様の事例 • ! 未ログインで使えるフォーム 53

Slide 54

Slide 54 text

まとめ • ! 今日の話はセキュリティの一部 • Railsガイドを読もう " • セキュリティに関する書籍 " • # 脆弱な仕様を断るのもエンジニアの仕事 • $ セキュリティは難しい 54