質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

90.34%

  • Ruby

    8172questions

    Rubyはプログラミング言語のひとつで、オープンソース、オブジェクト指向のプログラミング開発に対応しています。

  • Ruby on Rails

    7663questions

    Ruby on Railsは、オープンソースのWebアプリケーションフレームワークです。「同じことを繰り返さない」というRailsの基本理念のもと、他のフレームワークより少ないコードで簡単に開発できるよう設計されています。

  • CSRF

    47questions

    クロスサイトリクエストフォージェリ (Cross site request forgeries、CSRF)は、 外部Webページから、HTTPリクエストによって、 Webサイトの機能の一部が実行されてしまうWWWにおける攻撃手法です。

Rails4.2で複数モデルを一括登録しようとするとCSRF対策に引っ掛かる。というかパラメータが吹き飛んでいる。

受付中

回答 1

投稿 編集

  • 評価
  • クリップ 1
  • VIEW 712

17number

score 6

前提

作ろうとしているもの

  • RailsでWebアプリを開発中
  • User has_many Hobbies / Hobby belongs_to User な Hobby モデルを 1画面(1フォーム)で複数まとめて追加したい

開発環境

  • OS : Ubuntu 14.04.5 64bit(vagrant環境)
  • Ruby : 2.3.1
  • Rails : 4.2.7.1
$ cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=14.04
DISTRIB_CODENAME=trusty
DISTRIB_DESCRIPTION="Ubuntu 14.04.5 LTS"

$ uname -a
Linux vagrant-ubuntu-trusty-64 3.13.0-100-generic #147-Ubuntu SMP Tue Oct 18 16:48:51 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux

$ ruby -v
ruby 2.3.1p112 (2016-04-26 revision 54768) [x86_64-linux]

$ rails -v
Rails 4.2.7.1

開発経験・スキル

Rails経験は3ヶ月程度、実務(委託や自社サービスという意味)レベルでの経験はなく、Web系の開発経験も無し、個人で開発をしている程度のレベルです。そのレベルの人間の質問なので、おかしなことを言っていれば何でもご指摘頂けると助かります。

質問内容

今回起きたトラブルに対して色んな調査をしたため、本文がきれいにまとまっておらず申し訳ありません。長文となっていますが、なにかお気付きの点やアドバイスなどあれば、コメント頂けると幸いです。また、teratail初利用なので作法など守れていない点があるかもありません。

2017/02/24 16:36追記
検証に用いたコードをgithubに置いたので、コード中に気になる点などありましたら、ご指摘ください。
https://github.com/17number/rails-multiform-csrf-error

以下、質問本文になります。


User に紐づく Hobby を、1画面で3つ追加しようとした。参考にしたページは以下。

Rails4、fields_forを使って一括更新する処理のベストプラクティスは何だろう | もふもふ技術部

具体的なソースコードは以下、あまり関係なさそうな部分は割愛。

#app/models/user.rb
class User < ActiveRecord::Base
  has_many :hobbies
end

#app/models/hobby.rb
class Hobby < ActiveRecord::Base
  belongs_to :user
end

#app/views/hobbies/add_hobbies.html.slim
= form_tag commit_user_hobbies_path, method: :put do
  - @hobbies.each do |hobby|
    = fields_for "hobbies[]", hobby do |hf|
      = hf.label :name
      = hf.text_field :name
      br
  .actions = submit_tag

#app/controllers/hobbies_controller.rb
class HobbiesController < ApplicationController
  before_action :set_hobby, only: [:show, :edit, :update, :destroy]
  before_action :set_user, only: [:index, :new, :create, :edit, :update, :add_hobbies, :commit_hobbies]

  # GET /hobbies/multiadd
  def add_hobbies
    # 合計3つのHobbiesを追加したい
    (3 - @user.hobbies.length).times {@user.hobbies.build}
    @hobbies = @user.hobbies
  end

  # PUT /hobbies/multiadd
  def commit_hobbies
    # そもそも上手く動かないので具体的な処理は未実装
    binding.pry
    redirect_to user_hobbies_path(@user)
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_hobby
      @hobby = Hobby.find(params[:id])
    end

    def set_user
      @user = User.find(params[:user_id])
    end
end

#config/routes.rb
Rails.application.routes.draw do
  resources :users do
    resources :hobbies do
      collection do
        get "add_hobbies" => "hobbies#add_hobbies", as: :add, path: :multiadd
        match "add_hobbies" => "hobbies#commit_hobbies", as: :commit, path: :multiadd, via: [:post, :put]
      end
    end
  end
end

#db/schema.rb
ActiveRecord::Schema.define(version: 20170223020954) do
  create_table "hobbies", force: :cascade do |t|
    t.integer  "user_id",    limit: 4
    t.string   "name",       limit: 255
  end
  add_index "hobbies", ["user_id"], name: "index_hobbies_on_user_id", using: :btree

  create_table "users", force: :cascade do |t|
    t.string   "name",       limit: 255
  end
  add_foreign_key "hobbies", "users"
end

なぜかCSRF対策に引っ掛かる

このフォームを使った時、特定条件においてCSRF対策に引っ掛かる(Can't verify CSRF token authenticity)。
現状わかっている条件は以下の通り。

  • あらかじめ1つ、もしくは2つの Hobby が登録されている状態。つまり、見た目的には複数フォームの内、1つか2つのフォームがあらかじめ埋まっている状態。

なので、画面アクセス時の初期状態が以下であれば、CSRF対策には引っ掛かっていない。

  • 3つあるフォームが全て空っぽ
  • 3つあるフォームが全て埋まっている(あらかじめ3つの Hobby が登録されている)
CSRF対策に引っ掛かる原因調査

色々と調べた結果、以下のページに辿り着いたのでもう少し調べてみた。

RailsのCSRF対策の仕組みについて - Programming log - Shindo200
Rails4のCSRF対策で「Can't verify CSRF token authenticity」エラー - Qiita

verified_request?メソッドはパスできてるか?

verified_request? メソッドに binding.pry を仕込んでみたところ、本来ならパスするはずの以下をパスできていない。(※上手くいくケースは当該条件をパスしている事は確認済)

valid_authenticity_token?(session, form_authenticity_param)

form_authenticity_param = nil となっているのが原因。なお、form_authenticity_param は params から Token を取ってくるだけの模様。

# actionpack-4.2.7.1/lib/action_controller/metal/request_forgery_protection.rb
# The form's authenticity parameter. Override to provide your own.
def form_authenticity_param
  params[request_forgery_protection_token]
end
Tokenはフォームに埋まっているか

そもそも Token は form_tag や form_for を使えば自動で埋められるということで、当該画面のHTMLソースコードを確認したところ、 Token は埋まっていた。

protect_from_forgeryを使ってみたが根本解決せず

参考ページを見て、protect_from_forgery :except => [:commit_hobbies]を追加してみたところ、CSRF対策(Can't verify CSRF token authenticity)はパスできるようになった。

が、params の中身がスカスカになってしまっているため、期待動作(3つの Hobbies を登録する)は実現できなかった。

#上手くいくケース(これはAll blank状態からフォーム利用)
Rails4.2.7.1 Ruby2.3.1 pry(#<HobbiesController>)> params
=> {"utf8"=>"?",
 "_method"=>"put",
 "authenticity_token"=>"E4Yb/VmuFIi+q4+mdcZ4qoTyDBubl0Wjdz9hxTJ/+cHhkrt7cg2U+aim8l3r0CIyYUoMjs1phfwSz8CTALfLFA==",
 "hobbies"=>[{"name"=>"a"}, {"name"=>"b"}, {"name"=>"c"}],
 "commit"=>"Save changes",
 "controller"=>"hobbies",
 "action"=>"commit_hobbies",
 "user_id"=>"2"}

#ダメなケース(フォームが1つ埋まっている状態からフォーム利用)
Rails4.2.7.1 Ruby2.3.1 pry(#<HobbiesController>)> params
=> {"controller"=>"hobbies",
 "action"=>"commit_hobbies",
 "user_id"=>"2"}

そもそもコンソール上に表示されているParametersがおかしいっぽい

色々と見ていると、コンソール上の表示からしておかしい。authenticity_token など色々と足りていない模様。

# 上手くいくケース
Started PUT "/users/2/hobbies/multiadd" for 10.0.2.2 at 2017-02-23 12:48:46 +0900
Processing by HobbiesController#commit_hobbies as HTML
  Parameters: {"utf8"=>"?", "authenticity_token"=>"E4Yb/VmuFIi+q4+mdcZ4qoTyDBubl0Wjdz9hxTJ/+cHhkrt7cg2U+aim8l3r0CIyYUoMjs1phfwSz8CTALfLFA==", "hobbies"=>[{"name"=>"a"}, {"name"=>"b"}, {"name"=>"c"}], "commit"=>"Save changes", "user_id"=>"2"}

# ダメなケース(protect_from_forgery の有無に関わらずこんな感じ)
Started POST "/users/2/hobbies/multiadd" for 10.0.2.2 at 2017-02-23 12:50:38 +0900
Processing by HobbiesController#commit_hobbies as HTML
  Parameters: {"user_id"=>"2"}
  User Load (1.4ms)  SELECT  `users`.* FROM `users` WHERE `users`.`id` = 2 LIMIT 1

あらためて確認したところ、protect_from_forgery を追加しようがしまいが、上記のような内容だった。そもそも Token が含まれていないため、CSRF対策に引っ掛かっていたっぽい。

パケットキャプチャ上は正常に見える

じゃあ、Token はどこで消えてるのかと思い、開発環境上でパケットキャプチャも取得してみた。パケット中には Token や hobbies 相当のデータも入っている事を確認した。

その他 試したこと

form_tag の method 変更

色々とググっていく内に form_tag の method を変更したら状況変わるかも、と思い試してみました。が、結果としては同じでした。

試したケース

  • method=post(default) 
  • method=put

現状わかっていること

Rails内のどこかで何かの条件を満たせずに(or 満たしてしまい)、必要なパラメータ群が吹っ飛んでいるっぽい、というとこまでは分かったが、スキルと時間が足りないのでこの場で質問させてもらっています。

  • 上手くいかない原因は何なのか
  • どこを変更すれば良い
  • そもそもコードが間違っている

など、なにかコメント頂けると助かります。

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

質問への追記・修正、ベストアンサー選択の依頼

  • ryochin

    2017/02/24 16:08

    斜め読みですが、上手くいくケースは PUT でダメなケースは POST なのはなぜですか?

    キャンセル

  • 17number

    2017/02/24 16:23

    コメント、ありがとうございます。ご指摘頂いた method の違いについては気付いていませんでした。ただ、意図してそうなったものではなく、何故かそうなっていました。コメント頂いてから思い出したのですが、form_tag の method 変更も試したりもしました(が、変更してもしなくても結果は同じでした)。このログ自体は質問内容のソースで取得しているため、method 変更前と変更後の結果が混ざっている訳ではありません。念のため、試したこととして質問を修正しておきます。

    キャンセル

回答 1

+1

PHPですが、CSRF用のTokenでハマった例を。ご参考になれば幸いです。

  • ファイル選択フォームがあったため、methodがPUTとして扱われた
    ファイル選択フォームがあったため、enctypeに"multipart/form-data"としていた
    結果、$_POSTでなく$_FILESにTokenが入ってしまっていた
  • 設置したAjaxがサーバ側のTokenを更新してしまっていた
  • javascriptがTokenを消してしまっていた
  • そもそもformタグの中にTokenフォームが入っていなかった

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

同じタグがついた質問を見る

  • Ruby

    8172questions

    Rubyはプログラミング言語のひとつで、オープンソース、オブジェクト指向のプログラミング開発に対応しています。

  • Ruby on Rails

    7663questions

    Ruby on Railsは、オープンソースのWebアプリケーションフレームワークです。「同じことを繰り返さない」というRailsの基本理念のもと、他のフレームワークより少ないコードで簡単に開発できるよう設計されています。

  • CSRF

    47questions

    クロスサイトリクエストフォージェリ (Cross site request forgeries、CSRF)は、 外部Webページから、HTTPリクエストによって、 Webサイトの機能の一部が実行されてしまうWWWにおける攻撃手法です。

  • トップ
  • Rubyに関する質問
  • Rails4.2で複数モデルを一括登録しようとするとCSRF対策に引っ掛かる。というかパラメータが吹き飛んでいる。