railsのvalid?メソッドに潜んだ罠

railsのvalid?メソッドで、少し変わった仕様があったので備忘録として残しておきますー。

環境

rails1.2.3

序章

railsのvalid?メソッドは、意図しない挙動を起こすので、使う時には注意が必要です。
普通メソッド名の末尾に「?」をつけたメソッドは、メソッド内で真偽判定をして「true/false」で返してくれることを期待します。
しかし、valid?メソッドの中ではコチラが意図していない挙動が行われています。
てっきりvalid?メソッドは、レシーバのオブジェクトがsave可能かどうか(validationに引っかからないかどうか)を判定して「true/false」で返すものだと思っていました。(※1)
実際にメソッド名から想像すると、誰しもがその挙動を期待すると思います。
それが違うのです。。

内容

ある特定のアクションのみに適用させたいvalidateメソッド(以下、my_validateとする)を定義し、そのアクション内でmy_validateとvalid?メソッドを実行して、errorがないことをチェックしたいとします。
valid?メソッドが※1のような挙動であると信じている人は以下のようなコードを書くと思います。

# params[:test]はmy_validateに引っかかるとする

>> test = Test.new(params[:test])
>> test.my_validate
#=> false
>> test.errors.empty?
#=> false
def check_nedded_action
  test = Test.new(params[:test])
  test.my_validate

  if test.save
    true
  else
    false
  end
end

このように書くと不思議なことが起きます。
params[:new]の値ではmy_validationにひっかかるはず。
しかし、check_needed_actionにparams[:test]を渡すと「true」が返ってきます!

>> test = Test.new(params[:new])

>> test.my_validate
#=> false
>> test.errors.empty?
#=> false
>> test.valid?
#=> true
>> test.errors.empty?
#=> true

なぜ…??

答えはソースコードを見れば一目瞭然です。

# activerecord/lib/active_record/validations.rb
# Runs validate and validate_on_create or validate_on_update and returns true if no errors were added otherwise false.
777     def valid?
778       errors.clear
779 
780       run_validations(:validate)
781       validate
782 
783       if new_record?
784         run_validations(:validate_on_create)
785         validate_on_create
786       else
787         run_validations(:validate_on_update)
788         validate_on_update
789       end
790 
791       errors.empty?
792     end
793 
794     # Returns the Errors object that holds all information about attribute error messages.
795     def errors
796       @errors ||= Errors.new(self)
797     end

つまりはvalid?メソッドを呼び出した時点で、errorsオブジェクトをすでに持っている場合は、errors.clearされた後にvalidateを走らせるようになっているのです。
また、見てわかる通りvalid?メソッドの内部でvalidateを走らせて、errorがあった場合には、errorsオブジェクトの中にerrorを挿入しています。
そして、valid?メソッドはerrors.empty?で「true/false」を返り値として渡しているのです。

なるほどー。
これを受けて先ほどのコードを書き直すと、以下のようになるかと思います。

def check_nedded_action
  test = Test.new(params[:test])
  test.valid?
  test.my_validate

  if test.errors.full_messages.empty?
    test.save(false)
    true
  else
    false
  end
end

※saveメソッドでは保存の前にvalid?が呼び出されることになるので、敢えてsave(false)にしました

これで意図した挙動は保証できました!めでたしめでたし。

何だかスッキリしないですが、以上です。
もっとより良い実装方法があればFBお願いします!!!

最後までお読み頂き、ありがとうございます。

「なぜ危機に気づけなかったのか」を読んで

問題解決の前に問題発見が必要。その問題発見をするために必要なスキル、考え方の部分でとても勉強になったのでまとめておきます。
※かなり内容として広範囲な事柄が書かれていたので、自分が気になった部分だけ紹介していきます。

問題発見の7つのスキル

  • 情報のフィルターを避ける
  • 人類学者のように観察する
  • パターンを探し、見分ける
  • バラバラの点を線でつなぐ
  • 価値のある失敗を奨励する
  • 話し方と聴き方を訓練する
  • 行動を振り返り、反省のプロになる

問題の解決から問題の発見へ

包容力をもって問題を受け入れる
  • トヨタは問題を「学び、そして改善する機会」だと捉えている。
  • また、小さな問題をそれだけ取り出して単独で処理しているのではない。
  • 常にその問題をもっと大きなものに関連付けようとしている。
  • この小さな問題はより大きな障害の徴候となるものではないだろうか。何かシステム上の失敗が潜んでいるのではないだろうか。これがトヨタ問題意識だ。
問題を効果的に見つける能力を身につける
  • 問題の発見は、絶え間のない改善のプロセスの結果であることを肝に銘じておかなければならない。
  • 問題発見のプロセスと継続的な改善は密接につながっている。
    • 他のことを犠牲にして一つのことだけに集中すべきではないし、問題の発見が一直線に実績の改善に繋がると期待すべきでもない。
    • 古い問題を解決しようとしているときに、往々にして新たな問題を発見することがある。

情報のフィルターを避ける

確証バイアス
  • 情報のフィルタリングが比較的に無意識のうちに行われることがある。
  • 人は、自分が現在持っている意見や仮説と一致する情報を探し求める傾向があり、また特定の問題に対する自分の現在の姿勢を否定するようなデータを避けたがり、場合によっては無視することすらある。
  • このような傾向を、確証バイアスと呼んでいる。
    • この傾向を利用して人を動かす方法が「Yes」と答えられる質問を繰り返すことだと思う。
    • 相手が持っている意見と一致する部分から議論を進めていけば、確証バイアスが高まる。

人類学者になる

無意識
  • 発言と行動の食い違いが完全に無意識のうちに起きることがある。
  • 人がある状況でどのように振舞うかと尋ねられたとき、その人が通常答えるのはその状況にふさわしいと信じている行動理論だろう。
  • これはその人が信奉している行動理論、つまり建前としての理論であり、求められればその理論を他の人に伝えるだろう。
  • しかし、その人の行動を実際に支配しているのは実践理論、つまり持論である。
観察力に磨きをかける
  • 人と話をするときには、自由に答えられるような質問の仕方をし、誘導尋問にならないように心がけなければならない。
  • 積極的に聴く姿勢を貫く、つまりときどき聞いたことを復唱し、自分の解釈が正しいかどうかの確認を求め、間違っていれば説明してもらうようにする。
  • 観察が一段落したら学んだことを整理する必要がある。
  • 一緒に組んで観察した仲間がいるならメモを比較しあう時間をつくるべき。
    • 双方の観察結果や解釈に相違がないかどうかを調べ、もし違いがあれば同じ状況を違った視点で見ている理由を究明する。
    • 最後に、発見した問題点と改善のための機会を具体的に表にまとめる。
    • そして、その場にいなかった人に自分が学習した内容を投げかけて、相手の反応を見る。
    • 他に知りたいことがあるかどうかを尋ね、漏れがないかを確認する。

パターンを探す

理解するということはパターンを察知すること
直感とな何か?
  • 直感とは基本的にパターンを認識するプロセスである。
  • 人がある状況に直面したとき、過去の経験のパターンに当てはまるかどうかを判断する。
  • パターンを認識するプロセスは往々にして現在の状況と過去の状況の類似性を見つけることである。
  • 色々な状況に遭遇する回数が増えるに連れて、パターンを発見し、それを照合する能力が精密なものになっていく。
  • ※つまりはある事柄をパターン化することを意識的に行うようにしていれば、次に同じような状況に遭遇して時に瞬時に判断することができるようになる。
不完全な類似性
  • 人はいつも類推によって意思決定をしている。
  • 人は似ていると見なした二つの状況の類似性にこだわって、これを過大評価し、多くの基本的な違いを無視する傾向がある。
問題発見のソリューション
  • 問題発見をするときに「問題発見のソリューション(解決手法)」からスタートする場合に特定のトラブルに巻き込まれる。
  • 過去に成功したソリューションをもとに、そのソリューションが適用できる新しい問題点を探しては駄目。
  • これでは「金づちを持って釘を探すようなもの」である。本来は釘を発見し、その釘を打つために金づちを選択するのである。
  • 問題を発見するのにソリューションから始めてしまうと、類似点にはこだわるが、相違点を軽視するとい類推に特有の思考に陥ってしまう。
  • どんな状況でも常にパターンを探すべきだが、必ずしもそのパターンが正確に一致しているとは限らないことに注意が必要である。
パターンを認識する能力を高める
  • 類推力を高める
  • パターンを照合する能力を高めるための簡単な方法。
  • それは問題を麺滅に調べて「既知のこと」「不明瞭なこと」「推定したこと」の三つに区別することから始めること。
  • そうすれば暗黙の想定を明らかにでき、事実と想定の区別をつけざるを得なくなる。
    • 自分の想定を効果的に検証するには七つの質問が役に立つ。
1. この状況における事実はなにか
2. 曖昧な、あるいは不明瞭なことはなにか
3. 明確な推定と暗黙の推定と考えられることは何か
4. 事実と推定を混同していないか
5. 偏見のない考え方を持った外部の人は、自分の想定をどのように評価するだろうか
6. 重要な推定が間違っていることが分かれば、自分の結論も変わるだろうか
7. 単純な想定の正しさまたは間違いを立証するために、データを収集したり、簡単な実験をしたり、あるいは一定の分析をしたりすることが出来るだろうか

点を結びつける

想像力とは一見つながりのないもの同士を結びつける力である。
  • 情報の共有によって人々がわずかなデータの中から「点を結びつける」ことができ、それらが組み合わされ、統合されれば、組織における重大な潜在的問題の発見シグナルとなり得る。
「防止する」という心構え
  • 問題や状況をとことんまで考え抜く際に必要な能力は「統合的思考能力」と呼ばれる能力である。これは相反する、調和しないアイデアを合成する能力を磨くという意味である。
  • 優秀な統合的思考能力を持つ人の特徴は四つある。
1. ある状況における「それほど明らかではないが、関連性がありそうな要因」を広い範囲で、積極的に探す。複雑な問題を歓迎する。最善の回答が得られるのはそのような場合が多いから。
2. 単純に直線的な原因と結果の関係を考えない。ほとんどの結果は複数の原因によって生じることを認識しているから。
3. 「問題を全体として考える」個々の部分に切り離すのではなく、体系的に調べる。他の分野への影響力を重んじる。
4. 単純に二者択一な選択をしない。多くの断片的な情報だけでなく、異なった、また矛盾するアイデアを組み合わせ、つなぎあわせて革新的なアイデアを生む。

価値のある失敗を奨励する

容認できる失敗とできない失敗
  • この二つを見分けるためには、失敗をした本人が「その前」「その最中」「その後」にどういう行動をとったかを知る必要がある。
  • まず、その行動を取った本人の意思決定プロセスを理解しなければならない。
  • 次に、その行動が計画した進路を逸脱したとき、本人がいかなる反応を見せ、対応したのかを調べなければならない。
  • 最後に、その失敗の影響に対してどのような行動を取ったか、特にどこまで自分の責任を認め、その失敗からどれくらい学ぼうとしたかを評価する必要がある。
  • 失敗の前
    • 質の高い意思決定プロセス
1. 実行グループが複数の選択肢を考案し、その良否を評価する。
2. 鍵を握るであろう仮説を見付け出して、それをテストする。
3. 一般的な情報だけでなく、既存の概念の反証となる情報を収集している。
4. 先入観を持たない専門家に意見を聴く。
5. 最悪のシナリオを検討し、非常事態の計画を立てている。
    • 確認項目
1. 複数の選択肢の検討を怠っていなかったか?
2. 反対意見を押さえつけていなかったか?
3. 情報の集め方が偏っていなかったか?
4. 計画を実行に移す前に適切な実験やテストを行ったか?
  • 活動の最中
    • 様々な視点からの反応を収集し、当初の目的と目標に対する進展具合を定期的にチェックしていたか。
    • 否定的な反応が出てきたとき、外部条件が変化したときに当初の計画をきちんと修正したか。
  • 活動の後
    • まずは責任を素直に認めること。
    • その失敗を個人で済ませるのではなく、集団として組織として、同じ過ちを起こさないように教訓を組織全体で分かち合う。
    • 過去の失敗から学べることは多くある。定期的にチームや組織の失敗を振り返ると良い。

話し方と聴き方を訓練する

コミュニケーション上の過ち
  • 話し手
1. 重要な情報の脱落や偏った情報の提供がないか確認する。
2. 脱落が起きるのは話し手が急いでいるか、聞き手がすでに知っていると思い込むような場合。
3. ゆっくりと話したり抑揚をつけることで重要な点を強調する。
4. 重要なメッセージは繰り返し言葉にする。
5. 沈黙を同意と受け取っては駄目。
  • 聞き手
1. 相手が話し終わらないうちに結論を出しては駄目。
2. 相手が話している最中に、どう答えようかと考えては駄目。
3. 言葉以外の合図を見落としがち。
4. 自分が聞いたことを確認することが非常に重要。
5. 他のことを考えて、重要なことを聞き逃さないようにする。
率直で効果的な話し方
  • 問題を見つけた時に、それをはっきりと主張するための四段階のプロセスがある。
1. 自分の懸念を伝える相手には、名前で話しかける。
2. 自分の懸念を簡潔、明瞭かつ「感じたまま」に言い表す。一人称を使うと良い。
3. 問題の解決法を一つないし二つ提案する。考えられる範囲の解決策を提案することは、その問題への対処に手を貸す責任を担おうという意欲を伝えるシグナル。
4. 自分の意見について、相手の合意を取り付ける。
  • まず説得する相手のことを知る。
  • 次に相手はどういう考え方をし、どういう意思決定をするのかを知る。
  • 最後に、相手が証拠として求めているのはどんなものかを考えて提出する。
聴き方
  • 聞き取った内容を自分の言葉で言い換えて、正しく理解したかどうかを確かめる。聞き取った内容を記録化し、整理するために言葉や図表を使ってノートに書き記す。
  • 聞き手が話のある部分に非常に強く反応し、そのあとの部分を聞き逃すと、効果的な聞き方が全く出来なくなる。
  • 「完全に理解するまではいかなる評価も避ける」

行動を振り返り、反省のプロになる

事後検討会:期待と危険
  • 一連の出来事を手続きの流れとして図表化するなど、視覚的な助けを借りる。建設的で、事実に即した討論を促すのに役に立つ。
  • 「回想と検討」のプロセスでは、次の四つの基本的な疑問点に焦点を絞る。
1. 我々は何をすることを目指したのか
2. 実際には何が起きたのか
3. なぜそれが起きたのか
4. 次回はどのようにすれば良いか
デリベレイト・プラクティス
  • 特定の上達目標を設定し、すぐにフィードバックが得られるような仕組みを整える。
  • 自分がうまく実行できないことに焦点を絞る。
  • 一時に一つのことに集中するが、全体としては様々な技能を鍛えあげることを重視する。
  • プロセス重視。

優れた問題発見者の心構え

新しい心構えの三つの要素
  • 知的好奇心
    • 最も重要なことは、これまでの自分の判断や結論を疑ってかかることをいとわない姿勢。
    • 問題を発見するためには、時には曖昧さに立ち向かい、一見矛盾するような信号を読み解く能力が必要である。
    • また、乱雑な状況の中でその意味を理解する能力と、見慣れた状況を異なった視点から観察しようとする積極的な姿勢が要求される。
    • 目新しいものを学ぶという経験によって、新たな理論構成の枠組みだけでなく、見慣れた状況に対する考え方に関する新しい概念モデルが得られる。
  • システム思考
    • 問題を一歩離れたところから見て、なぜミスが起きたのかに疑問を持つ。
    • もっと基本的な組織上の問題が、些細なミスが起きやすいような条件を生み出したのではないか?と考える。
    • 明らかに問題だとわかるものの背後に何が潜んでいるのだろうか。技術的な過失はあるだろうか。
  • 健全な偏執狂
    • いかに成功していようとも、すべての組織は数々の問題を抱えていることを承知している。
    • 自分が過ちを犯しやすい存在であることを承知しており、無敵だというオーラを身につけようとしない。
    • 問題は問題がないと思っていることであり、問題を抱えていることが問題だと考えていること。


たくさん勉強させて頂きました。何度か繰り返し読みたい本です。
最後までお読み下さり、ありがとうございました。

なぜ危機に気づけなかったのか ― 組織を救うリーダーの問題発見力

なぜ危機に気づけなかったのか ― 組織を救うリーダーの問題発見力

【ruby】Dateクラスで正しい暦日付かどうかを判定するには

今日はrubyの話。
rubyのDateクラスを用いて、正しい日付かどうかを判定する方法。
一応実装は出来ていますが、あまり良い解決策でないように感じるので、ご助言頂けると嬉しいです!

# 今日の日付を取得

date = Date.today
# 引数で渡した年月日が正しい日付かどうかを判定する。
# 正しい暦日付であれば、相当するユリウス日を返す。そうでないなら、nilを返す。
# ちなみに、valid_civil?メソッドを使っても全く同じ結果になる。

if Date.valid_date?(2000, 8, 8)
  p "true"
else
  p "false"
end

#=> "true"

if Date.valid_date?(2000, 8, 38)
  p "true"
else
  p "false"
end

#=> "false"
# params[:date]で文字列として日付が渡される場合。
# 例えば、"2000-08-08"など。

date = Date.parse(params[:date])

# parseできないパラメータ"2000-08-38"などが送られてくるとエラーを起こしてしまう。
# 以下は"0000-00-00"という形でパラメータが送られてくるという前提のもとでの実装である。
# 正しい暦日付であれば、parseされてDateオブジェクトが返る。
# 正しくない暦日付であれば、falseが返る。

date_ary = params[:date].split(/-/)      #=> ["2000", "08", "38"]

if Date.valid_date?(date_ary[0].to_i, date_ary[1].to_i, date_ary[2].to_i)
  Date.parse(params[:date])
else
  return false
end
  • これで一応、不正な日付パラメータのチェックはできた。
  • 懸念点
    • 前提として"0000-00-00"という形で送られてくることを想定している。
    • コードに無理やり感が否めない。

より良いコードがあればご教示頂けますと幸いですm(__)m

最後までお読み下さり、ありがとうございました。

【rails】create/update時に特有のvalidationの実装

RailsActiveRecordのお話です。
recordを新しく作成する時にだけかけたいvalidationがあったり、更新する時にだけかけたいvalidationがあった経験はありませんか?
そんな時、ActiveRecordはとってもお利口さん。
対象のクラスにおいて、魔法のメソッド「validate_on_create」「validate_on_update」メソッドを定義してあげると、上記を実現できます。

  • 例えば、以下のような状況設定とする。
Studentテーブル
└ 作成するときだけ、パスワード(password)を確実に登録してもらう。
└ 更新する時だけ、年齢(old)を入力してもらう。
  • 実現方法は多数ありますが、代表的なものを2つほど紹介します。

実現方法1

class Student < ActiveRecord::Base
  validates_presence_of :password, :if => Proc.new{|p| |p.new_record?}
  validates_presence_of :old, :if => Proc.new{|p| !p,new_record?}
end
  • ちなみに、このifの条件が複雑になった場合はProcを使わず、メソッドを定義してあげることも可能
class Student < ActiveRecord::Base
  validates_presence_of :password, :if => :password_required?
  
  def password_required?
    #判定ロジック
  end
end

実現方法2

class Student < ActiveRecord::Base
  def validate_on_create
    validates_presence_of :password
  end

  def validate_on_update
    validates_presence_of :old
  end
end
  • 個人的には条件が複雑にならない限り、作成/更新時に特有なvalidation1を実装する場合は実装方法2で実装する方が好きです。
  • ちなみに、validate_on_createやvalidate_on_updateを定義するだけこのことが実現できるのかが謎だったので、ソースコード読んでみました。

validate_on_updateの仕組み

  • 前提としてARオブジェクトをsaveする際、以下のcallbacksが呼び出されます。
(-) save
(-) valid?
(1) before_validation
(2) before_validation_on_create
(-) validate
(-) validate_on_create
(3) after_validation
(4) after_validation_on_create
(5) before_save
(6) before_create
(-) create
(7) after_create
(8) after_save
  • この中で、今回はvalid?メソッドに着目して、validations.rbを見ていきます。
    • callbacksはまたの機会にまとめます。
# https://github.com/rails/rails/blob/v1.2.3/activerecord/lib/active_record/validations.rb#L786
  def valid?
    errors.clear

    run_validations(:validate)
    validate

    if new_record?
      run_validations(:validate_on_create)
      validate_on_create
    else
      run_validations(:validate_on_update)
      validate_on_update
    end

    errors.empty?
  end

# https://github.com/rails/rails/blob/v1.2.3/activerecord/lib/active_record/validations.rb#L823
  def run_validations(validation_method)
    validations = self.class.read_inheritable_attribute(validation_method.to_sym)
    if validations.nil? then return end
    validations.each do |validation|
      if validation.is_a?(Symbol)
        self.send(validation)
      elsif validation.is_a?(String)
        eval(validation, binding)
      elsif validation_block?(validation)
        validation.call(self)
      elsif validation_class?(validation, validation_method)
        validation.send(validation_method, self)
      else
        raise(
          ActiveRecordError,
          "Validations need to be either a symbol, string (to be eval'ed), proc/method, or " +
          "class implementing a static validation method"
        )
      end
    end
  end
  • valid?メソッドを見るとわかる通り、まずはvalidateメソッドが走り、次に対象がnew_recordかどうか判定してvalidate_on_createかvalida_on_updateを走らせるかを決定しています。
  • つまり簡単な流れは以下のような流れになる。
    • ARのsaveが呼び出される
    • valid?メソッドが呼び出される
    • validatesメソッドが呼び出される
    • new_record?かどうかを判定する
    • validate_on_create / validate_on_updateメソッドが呼び出される
  • このように定義されているために、ARを継承しているStudentクラスでvalidate_on_updateメソッドを定義すると、更新時にだけ「validates_presence_of」が呼び出された訳ですね!
    • ちなみに、validates_presence_ofのメソッドも面白かったので転記
# https://github.com/rails/rails/blob/v1.2.3/activerecord/lib/active_record/validations.rb#L400
  def validates_presence_of(*attr_names)
    configuration = { :message => ActiveRecord::Errors.default_error_messages[:blank], :on => :save }
    configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)

     # can't use validates_each here, because it cannot cope with nonexistent attributes,
     # while errors.add_on_empty can
     attr_names.each do |attr_name|
       send(validation_method(configuration[:on])) do |record|
         unless configuration[:if] and not evaluate_condition(configuration[:if], record)
           record.errors.add_on_blank(attr_name,configuration[:message])
         end
       end
     end
   end
    • errors.add_on_blank?って何だ?今までerrors.addしか知らなかったよ。。
# https://github.com/rails/rails/blob/v1.2.3/activerecord/lib/active_record/validations.rb#L63
  def add_on_blank(attributes, msg = @@default_error_messages[:blank])
    for attr in [attributes].flatten
      value = @base.respond_to?(attr.to_s) ? @base.send(attr.to_s) : @base[attr.to_s]
      add(attr, msg) if value.blank?
    end
  end
    • 要はvalueがblank?の時にaddする訳ですね。fmfm。どこかで使えそう!

「安全なウェブサイトの作り方」改訂第5版を読んで

「安全なウェブサイトの作り方」は、IPAが届出を受けた脆弱性関連情報を基に、届出件数の多かった脆弱性や攻撃による影響度が大きい脆弱性を取り上げ、ウェブサイト開発者や運営者が適切なセキュリティを考慮したウェブサイトを作成するための資料です。それが今回、携帯ウェブサイトの実装方法を追加した第5版が公表されました。

  • 第1章「ウェブアプリケーションのセキュリティ実装」
    • SQLインジェクション 、OSコマンド・インジェクション やクロスサイト・スクリプティング 等9種類の脆弱性を取り上げ、それぞれの脆弱性で発生しうる脅威や特に注意が必要なウェブサイトの特徴等を解説
    • 脆弱性の原因そのものをなくす根本的な解決策、攻撃による影響の低減を期待できる対策を示しています
  • 第2章「ウェブサイトの安全性向上のための取り組み」
    • ウェブサーバのセキュリティ対策やフィッシング詐欺を助長しないための対策等7つの項目を取り上げ、主に運用面からウェブサイト全体の安全性を向上させるための方策を示しています
  • 第3章「失敗例」
  • 巻末

※PDF資料は以下のサイトでダウンロードすることができます。



こちらの資料ではPDFでしか見ることが出来ず、少し読みづらいしチェックしずらいと思ったので、簡単に1章と2章だけまとめてみました。

第1章「ウェブアプリケーションのセキュリティ実装」

  • 自分が選択する対策が、どのような性質を持っているのか、期待する効果が得られるものなのか、ということを正しく理解、把握することが大事
  • アプリケーションにおける脆弱性対策は2つに分かれる。「根本的解決」と「保険的対策」である
    • 1.根本的解決
      • 脆弱性を作りこまない実装を実現する方法。これによって脆弱性を狙った攻撃が無効化されることが期待される。
    • 2.保険的対策
      • 攻撃による影響を軽減する方法。原因そのものをなくすものではないが、攻撃から被害までの各フェーズにおいて、それぞれの影響を軽減できる。
      • 基本的には根本的解決が望ましいが、すでに作ってしまったアプリケーションであったりコストの観点から併用する必要がある。
      • ただ、保険的対策は対策の内容によっては本来の機能を制限することになるものもあるので、そのような副作用の影響も考慮する必要がある。
具体的なセキュリティの問題点
  • 1.SQLインジェクション
    • ◆根本的解決
      • SQL文の組み立ては全てプレースホルダで実装する。
      • 値を文字列型として埋め込む場合は、値をシングルクォートで囲んで記述しますが、その際に文字列リテラル内で特別な意味を持つ記号文字をエスケープ処理します(たとえば、「'」→「''」、「\」→「\\」等)。形で埋め込みます。
      • この処理は外部からの入力の影響を受ける値のみに限定して行うのではなく、SQL文を構成する全てのリテラル生成に対して行うべき。
    • ◆保険的対策
      • データベースアカウントに適切な権限を与える
      • ウェブアプリケーションがデータベースに接続する際に使用するアカウントの権限が必要以上に高い場合、攻撃による被害が深刻化する恐れがあります。
      • ウェブアプリケーションからデータベースに渡す命令文を洗い出し、その命令文の実行に必要な最小限の権限をデータベースアカウントに与えてください。
      • ※文字列リテラルエスケープ処理
        • 文字列リテラル中にシングルクォートが現れる場合には、シングルクォートを重ねて記述することで文字としてのシングルクォートを表すというのがSQLの文法
  • 2.OSコマンドインジェクション
    • ◆根本的解決
      • シェルを起動できる言語機能の利用を避ける
        • 例)Perlのopen関数は引数として与えるファイルパスに「|」を使うことでOSコマンドを実行できるため、外部かの入力値を引数として利用する実装は危険。
        • sysopen関数であればシェルを起動することはない
    • ◆保険的対策
      • シェルを起動できる言語機能を利用する場合は、その引数を構成する全ての変数に対してチェックを行い、あらかじめ許可した処理のみを実行する。
外部からのパラメータにウェブサーバ内のファイル名を直接指定している場合、
ファイル名指定の実装に問題がある場合、攻撃者に任意のファイルを指定され、
ウェブアプリケーションが意図しない処理を行ってしまう可能性がある。
    • ◆根本的解決
      • 外部からのパラメータでウェブサーバ内のファイル名を直接指定する実装を避ける
      • ファイルを開く際は、固定のディレクトリを指定し、かつファイル名にディレクトリ名が含まれないようにする
      • これを回避するために、basename()等の、パス名からファイル名のみを取り出すAPIを利用して、open(dirname+basename(filename))のような形でコーディングして、filenameに与えら れたパス名からディレクトリ名を取り除くようにする
    • ◆保険的対策
      • ウェブサーバ内のファイルへのアクセス制限の設定うぃ正しく管理する
      • ファイル名のチェックを行う
  • 4.セッション管理の不備
・セッションIDの生成規則を割り出し、有効なセッションIDを推測する
・罠を仕掛けたり、ネットワークを盗聴したりして、利用者のセッションIDを盗む
・セッションIDの固定化
 └何らかの方法で自分が取得したセッションIDを利用者に送り込み、
    利用者のログインを狙って、その利用者になりすます。
    • ◆根本的解決
      • セッションIDを憶測が困難なものにする
      • セッションIDをURLパラメータに格納しない
        • 利用者のブラウザがReferer送信機能によってセッションIDの含まれたURLをリンク先のサイトへ送信してしまう。
        • セッションIDはCookieに格納するか、POSTメソッドのhiddenパラメータに格納して受け渡しするようにする
        • ウェブアプリケーションサーバ製品によっては利用者がCookieの受け入れを拒否している場合、セッションIDをURLパラメータに格納する実装に自動的に切り替えてしまうものがある。そのような機能は、製品の設定変更を行う等によって自動切り替え機能を無効化する。
      • HTTPS通信で利用するCookieにはsecure属性を加える
        • ウェブサイトが発行するCookieにはsecure属性という項目があり、これが設定されたCookieHTTPS通信のみで利用されます。
        • Cookieにsecure属性がない場合、HTTPS通信で発行したCookieは、経路が暗号化されていないHTTP通信でも利用されるため、このHTTP通信の盗聴によりCookie情報を不正に取得されてしまう可能性があります。
        • HTTPS通信で利用するCookieにはsecure属性を必ず加えてください。かつ、HTTP通信でCookieを利用する場合は、HTTPSで発行するCookieとは別のものを発行してください。
      • ログイン成功後に新しくセッションを開始する
      • ログイン成功後に、既存のセッションIDとは別に秘密情報を発行し、ページの遷移ごとにその値を確認する(※セッションIDをログイン前に発行している場合)
      • セッションIDとは別に、ログイン成功時に秘密情報を作成してCookieにセットし、この秘密情報とCookieの値が一致することを全てのページで確認するようにします。
    • ◆保険的対策
      • セッションIDを固定値にしない
        • 発行するセッションIDが利用者ごとに固定の値である場合、この情報が攻撃者に入手されると、時間の経過に関係なく、いつでも攻撃者からセッション・ハイジャックされてしまいます。セッションIDは、利用者のログインごとに新しく発行し、固定値にしないようにしてください。
      • セッションIDをCookieにセットする場合、有効期限の設定に注意する
        • Cookieは有効期限が過ぎるまでブラウザに保持されます。このため、ブラウザの脆弱性を悪用する等何らかの方法でCookieを盗むことが可能な場合、その時点で保持されていたCookieが盗まれる可能性があります。
        • Cookieを発行する場合は、有効期限の設定に注意してください。たとえば、Cookieの有効期限を短い日時に設定し、必要以上の期間、Cookieがブラウザに保存されないようにする等の対策をとります。
        • なお、Cookieをブラウザに残す必要が無い場合は、有効期限の設定(expires=)を省略し、発行したCookieをブラウザ終了後に破棄させる方法もあります。しかし、この方法は、利用者がブラウザを終了させずに使い続けた場合にはCookieは破棄されないため、期待する効果を得られない可能性があります。
ウェブアプリケーションにスクリプトを埋め込むことが可能な脆弱性がある場合、
これを悪用した攻撃により、利用者のブラウザ上で不正なスクリプトが実行されてしまう可能性がある

HTMLテキストの入力を許可しない場合の対策

    • ◆根本的解決
      • ウェブページに出力する全ての要素に対して、エスケープ処理を施す
        • 脆弱性防止の観点からエスケープ処理が必須となるのは、外部からウェブアプリケーションに渡される「入力値」の文字列や、データベースやファイルから読み込んだ文字列、その他、何らかの文字列を演算によって生成した文字列等です。
        • しかし、必須であるか不必要であるかによらず、テキストとして出力するすべてに対してエスケープ処理を施すよう、一貫したコーディングをすることで、対策漏れ17を防止することができます。
        • なお、対象となる出力処理はHTTPレスポンスへの出力に限りません。JavaScriptのdocument.writeメソッドやinnerHTMLプロパティ等を使用して動的にウェブページの内容を変更する場合も、上記と同様の処理が必要です。
      • URLを出力する際は「http://」「https://」で始まるURLのみを許可する
        • URLには、「http://」や「https://」から始まるものだけでなく、「javascript:」の形式で始まるものもあります。
        • ウェブページに出力するリンク先や画像のURLが、外部からの入力に依存する形で動的に生成される場合、そのURLにスクリプトが含まれていると、クロスサイト・スクリプティング攻撃が可能となる場合があります。
        • たとえば、利用者から入力されたリンク先のURLを「」の形式でウェブページに出力するウェブアプリケーションは、リンク先のURLに「javascript:」等から始まる文字列を指定された場合に、スクリプトを埋め込まれてしまう可能性があります。
        • リンク先のURLには「http://」や「https://」から始まる文字列のみを許可する、「ホワイトリスト方式」で実装してください。
      • 要素の内容を動的に生成しない
      • スタイルシートを任意のサイトから取り込めるようにしない
    • ◆保険的対策
      • 入力値の内容チェックを行う
        • 入力チェック機能をウェブアプリケーションに実装し、条件に合わない値を入力された場合は、処理を先に進めず、再入力を求めるようにします。
        • ただし、この方法ではチェックを通過した後の演算処理の結果がスクリプト文字列を形成してしまう場合等には対処できないため、この対策のみに頼ることはお勧めできません。

HTMLテキストの入力を許可する場合の対策

    • ◆根本的解決
      • 入力されたHTMLテキストから構文解析木を作成し、スクリプトを含まない必要な要素のみを抽出する
    • ◆保険的対策
      • 入力されたHTMLテキストからスクリプトに該当する文字列を排除する

全てのアプリケーションに共通の対策

    • ◆根本的解決
      • HTTPレスポンスヘッダのContent-Typeフィールドに文字コード(charset)を指定する
        • Content-Typeフィールドで文字コードの指定を省略した場合、攻撃者が、この挙動を悪用して、故意に特定の文字コードをブラウザに選択させるような文字列を埋め込んだ上、その文字コードで解釈した場合にスクリプトのタグとなるような文字列を埋め込む可能性があります。
    • ◆保険的対策
      • Cookie情報の漏洩対策として、発行するCookieにHttpOnly属性を加え、TRACEメソッドを無効化する
        • 「HttpOnly」は、Cookieに設定できる属性のひとつで、これが設定されたCookieは、HTMLテキスト内のスクリプトからのアクセスが禁止されます。これにより、ウェブサイトにクロスサイト・スクリプティング脆弱性が存在する場合であっても、その脆弱性によってCookieを盗まれるという事態を防止できます。
        • 具体的には、Cookieを発行する際に、「Set-Cookie:(中略)HttpOnly」として設定します。
        • なお、この対策を採用する場合には、いくつかの注意が必要です。
        • まず、ウェブサーバにおいて「TRACEメソッド」を無効とする必要があります。
        • 「TRACEメソッド」が有効である場合、サイトにクロスサイト・スクリプティング脆弱性があると、「Cross-Site Tracing」と呼ばれる攻撃手法によって、ブラウザから送信されるHTTPリクエストヘッダの全体が取得されてしまいます。
        • HTTPリクエストヘッダにはCookie情報も含まれる20ため、HttpOnly属性を加えていてもCookieは取得されてしまいます。
        • また、HttpOnly属性は、ブラウザによって対応状況に差がある21ため、全てのウェブサイト閲覧者に有効な対策ではありません。
        • 本対策は、クロスサイト・スクリプティング脆弱性のすべての脅威をなくすものではなく、Cookie漏えい以外の脅威は依然として残るものであること、また、利用者のブラウザによっては、この対策が有効に働かない場合があることを理解した上で、対策の実施を検討してください。
  • 6.CSRF(クロスサイト・リクエスト・フォージェリ)
ログインした利用者からのリクエストについて、その利用者が意図したリクエストであるかどうかを
識別する仕組みを持たないウェブサイトは、外部サイトを経由した悪意のあるリクエストを受け入れてしまう場合が
あります。このようなウェブサイトにログインした利用者は、悪意のある人が用意した罠により、
利用者が予期しない処理を実行させられてしまう可能性があります。
このような問題を「CSRF(Cross-Site Request Forgeries/クロスサイト・リクエスト・フォージェリ)の脆弱性」
と呼び、これを悪用した攻撃を、「CSRF攻撃」と呼びます。
    • ◆根本的解決
      • 処理を実行するページをPOSTメソッドでアクセスするようにし、その「hiddenパラメータ」に秘密情報が挿入されるよう、前ページを自動生成して、実行ページではその値が正しい場合のみ処理を実行する
        • 具体的な例として、「入力画面→確認画面→登録処理」のようなページ遷移を取り上げて説明します。
        • まず、利用者の入力内容を確認画面として出力する際、合わせて秘密情報を「hidden パラメータ」に出力するようにします。
        • この秘密情報は、セッション管理に使用しているセッションIDを用いる方法の他、セッションIDとは別のもうひとつのID(第2セッションID)をログイン時に生成して用いる方法等が考えられます。
        • 生成するIDは暗号論的擬似乱数生成器を用いて、第三者に予測困難なように生成する必要があります。
        • 次に確認画面から登録処理のリクエストを受けた際は、リクエスト内容に含まれる「hiddenパラメータ」の値と、秘密情報とを比較し、一致しない場合は登録処理を行わないようにします23。
        • このような実装であれば、攻撃者が「hiddenパラメータ」に出力された秘密情報を入手できなければ、攻撃は成立しません。
        • なお、このリクエストは、POSTメソッドで行うようにします24。これは、GET メソッドで行った場合、外部サイトに送信されるRefererに秘密情報が含まれてしまうためです。
      • 処理を実行する直前のページで再度パスワードの入力を求め、実行ページでは、再度入力されたパスワードが正しい場合のみ処理を実行する
      • Refererが正しいリンク元かを確認し、正しい場合のみ処理を実行する
    • ◆保険的対策
      • 重要な操作を行った際に、その旨を登録済みのメールアドレスに自動送信する
  • 7.HTTPヘッダ・インジェクション
ウェブアプリケーションの中には、リクエストに対して出力するHTTPレスポンスヘッダのフィールド値を、
外部から渡されるパラメータの値等を利用して動的に生成するものがあります。
たとえば、HTTPリダイレクションの実装として、パラメータから取得したジャンプ先のURL情報を、
Locationヘッダのフィールド値に使用する場合や、掲示板等において入力された名前等をSet-Cookieヘッダの
フィールド値に使用する場合等が挙げられます。このようなウェブアプリケーションで、HTTPレスポンスヘッダの
出力処理に問題がある場合、攻撃者は、レスポンス内容に任意のヘッダフィールドを追加したり、
任意のボディを作成したり、複数のレスポンスを作り出すような攻撃を仕掛けること。
※HTTPレスポンス分割とキャッシュ汚染
分割されたレスポンスがキャッシュサーバにキャッシュされ、このサイトの利用者が差し替えられた
嘘のウェブページを閲覧してしまうこと
    • ◆根本的解決
      • ヘッダの出力を直接行わず、ウェブアプリケーションの実行環境や言語に用意されているヘッダ出力用APIを使用する
      • 改行コードを適切に処理するヘッダ出力用APIを利用できない場合は、改行を許可しないよう、開発者自身で適切な処理を実装する
    • ◆保険的対策
      • 外部からの入力の全てについて、改行コードを削除する
  • 8.メールヘッダ・インジェクション
メール送信機能を持つウェブアプリケーションに問題がある場合、
管理者が設定した本来固定のメールアドレスではない宛先にメールを送信され、
迷惑メールの送信に悪用される可能性がある。
    • ◆根本的解決
      • メールヘッダを固定値にして、外部からの入力は全てメール本文に出力する
      • メールヘッダを固定値にできない場合、ウェブアプリケーションの実行環境や言語に用意されているメール送信用APIを使用する
      • HTMLで宛先を指定しない
    • ◆保険的対策
      • 外部からの入力の全てについて、改行コードを削除する
  • 9.アクセス制御や認可制御の欠落

アクセス制御の欠落

    • ◆根本的解決
      • アクセス制御機能による防御措置が必要とされるウェブサイトには、パスワード等の秘密情報の入力を必要とする認証機能を設ける。

認可制御の欠落

    • 認証機能に加えて認可制御の処理を実装し、ログイン中の利用者が他人になりすましてアクセスできないようにする。


第2章「ウェブサイトの安全性向上のための取り組み」

  • ◆ウェブサーバのセキュリティ対策
    • OSやソフトウェアの脆弱性情報を継続的に入手し、脆弱性への対処を行う
    • ウェブサーバをリモート操作する際の認証方法として、パスワード認証以外の方法を検討する(公開鍵認証など)
    • パスワード認証を利用する場合は、十分に複雑な文字列を指定する
    • 不要なサービスやアカウントを停止、削除する
    • 公開を設定していないファイルをウェブ公開用のディレクトリ以下に置かない
  • DNS情報の設定不備
    • ドメイン名およびそのDNSサーバの登録状況を調査し、必要に応じて対処を行う
  • ◆ネットワーク盗聴への対策
    • 重要な情報を取り扱うウェブページでは、通信経路を暗号化する
      • 通信を暗号化する主な手段として、SSL(Secure Socket Layer)やTLS(Transport Layer Security)を用いたHTTPS通信の利用があります。
      • 個人情報の登録ページや認証情報をリクエストするログインページ等、保護するべき情報を扱うウェブサイトでは、通信経路を暗号化することをお勧めします。
    • 利用者へ通知する重要情報は、メールで送らず、暗号化されたhttps://のページに表示する
    • ウェブサイト運営者がメールで受け取る重要情報を暗号化する
      • ウェブページに入力された個人情報等の重要情報を、ウェブアプリケーションに実装されたメール通知機能を利用して、ウェブサイト運営者がメールで受け取る場合は、S/MIMEPGP等を利用してメールを暗号化するようにしてください。
      • S/MIMEPGPを利用できない場合には、その他の方法でメール本文を暗号化するようにします。
      • なお、盗聴対策として、メールサーバ間の通信の暗号化(SMTP over SSL)やメールサーバとウェブサイト運営者との通信の暗号化(POP/IMAP over SSL)等も考えられますが、ネットワーク構成によっては、途中経路が暗号化されない可能性があるため、安全とは言えません。
  • ◆パスワードの不備
    • 初期パスワードは、憶測が困難な文字列で発行する
    • パスワードの変更には、現行パスワードの入力を求める
    • 入力後の応答メッセージが認証情報の憶測のヒントとならない工夫をする
    • パスワード入力のフォームでは、入力文字列を伏字で表示する
  • フィッシング詐欺を助長しないための対策
    • 実在証明書付きのSSLサーバ証明書を取得し、サイトの運営者が誰であるかを証明する
    • フレームを利用する場合、子フレームのURLを外部パラメータから生成しないように実践する
    • 利用者がログイン後に移動するページをリダイレクト機能で動的に実装しているウェブサイトについて、リダイレクト先のURLとして使用されるパラメータの値には、自サイトのドメインのみを許可するようにする
WAFは、ウェブアプリケーションを含むウェブサイトと利用者の間で交わされるHTTP(HTTPS通信を含む32)を検査し、
攻撃等の不正な通信を自動的に遮断するソフトウェア、もしくはハードウェアです。
WAFを使用することで以下の効果を期待できます。
1. 脆弱性を悪用した攻撃からウェブアプリケーションを防御する
2. 脆弱性を悪用した攻撃を検出する
3. 複数のウェブアプリケーションへの攻撃をまとめて防御する

WAFにおけるHTTP通信の検査

      • WAFはWAFを導入したウェブサイト運営者が設定する検出パターンに基づいて、ウェブサイトと利用者の間で交わされるHTTP通信内のHTTPリクエスト、HTTPレスポンスそれぞれの中身を機械的に検査します。
      • WAFは、検査の結果からHTTP通信がウェブサイト、利用者にとって「悪いもの」かどうかを判定します。
      • 検出パターンには、「ウェブアプリケーション脆弱性を悪用する攻撃に含まれる可能性の高い文字列」や「ウェブアプリケーション仕様で定義されているパラメータの型、値」といったものを定義します。
      • WAFがHTTP通信を「正常である」と判定した場合(陰性判定)、検査したHTTP通信を利用者またはウェブサイトにそのまま送信します。
      • 一方、WAFがHTTP通信を「悪質である」と判定した場合(陽性判定)には、WAFは検査したHTTP通信を送信せずに設定された処理(管理者への警告、該当通信の遮断等)を実行します。
      • WAFはHTTP通信を機械的に検査しているため、人の目で見ると間違った判断となる陰性判定、陽性判定(以降、判定エラー)が生じる場合があります。

HTTP通信の検査における判定エラー

      • HTTP通信の中身によっては、判定エラーが生じる場合があります。判定エラーには偽陽性偽陰性の2種類があります。
      • 偽陽性とは、本来「正常である」にもかかわらず、「悪質である」と判定されるエラーです。英語では一般的にfalse positiveと呼ばれます。
      • 偽陰性とは、本来「悪質である」にもかかわらず、「正常である」と判定されるエラーです。英語では一般的にfalse negativeと呼ばれます。
      • WAFを使用する場合、偽陽性(false positive)、偽陰性(false negative)の判定が生じる可能性を考慮する必要があります。

WAFの導入検討における留意点

      • WAFを導入するに際して、偽陽性偽陰性の判定が生じる可能性を低くするためには、まず、WAFが検出パターンに合致するHTTP通信を検出してもHTTP通信を遮断しないように設定し、HTTP通信を監視するだけのテスト期間を設けます。
      • このテスト期間にWAFの保護対象ウェブアプリケーションを実際に使用して正当なHTTP通信が遮断されないか、また保護対象ウェブアプリケーションにあわせてWAFの検出パターンを適切に設定しているか、といったWAFの動作確認を実施します。
      • この動作確認を実施するには、保護対象ウェブアプリケーションの理解やHTTP通信に関連したプロトコルの専門的知識が要求され、かつ十分な作業工数が必要です。そのため、外部の専門家にWAFの導入を依頼することも検討してください。

携帯ウェブ向けのサイトにおける注意点

  • ◆セッション管理に関する注意点
    • Cookie機能がない機種では、セッション管理のためセッションIDをURLに格納せざるを得ませんでした。
    • 一般的には、セッションIDをURLパラメータに格納していると、利用者のブラウザが、Referer送信機能によって、セッションIDの含まれたURLをリンク先のサイトへ送信してしまい、セッション・ハイジャック攻撃につながる危険があります。
    • そのため、携帯ウェブ向けのサイトでは、外部サイトへのリンクを作らないようにするか、外部サイトへのリンクを作る場合であっても、URLにセッションIDを含まないページを間に挟むようにする等の対策が取られているようです。
    • しかし、その場合でも、利用者が自らURLを公開したこと等が原因となって、そのURLのページが検索エンジンに登録されることによる個人情報漏洩事故が発生していることから、根本的な解決にはなっていません。
    • 可能な限りこのような実装は避けるべきであり、Cookie機能に非対応のキャリアにだけ上記の回避策をとり、それ以外のキャリアに対してはCookie機能を用いて通常の一般的なPC向けウェブサイトと同様に実装するのが適切でした。
    • しかし、2009年5月以降、同じキャリアでもCookie機能に非対応の機種と対応する機種が混在するようになったため、キャリア単位ではなく、機種ごとに実装方法を分けるべきと言えます。
  • ◆携帯ID(個体識別番号)の使用に関する注意点
    • 携帯IDの正式名称はキャリアによって異なり、代表的なものに「iモードID」、「EZ番号」、「ユーザID」、「FOMA端末製造番号」、「FOMAカード製造番号」、「端末シリアル番号」などがあります。
    • 携帯IDには、次のような特徴があります。
      • すべてのウェブサイトに同じ携帯IDが通知される。
      • キャリアの公式サイトでなくとも通知される。
      • HTTPリクエストヘッダ(User-Agentヘッダまたは、キャリア独自の拡張ヘッダ)に格納されて、ウェブサイトに通知される。
      • 利用者の設定変更により、通知を停止することができるが、初期設定では通知される。
  • ◆携帯IDによる脆弱な認証
    • ウェブサイトによっては、携帯IDだけで利用者を認証する設計のものがあります。このような認証方式は、しばしば「かんたんログイン」と呼ばれます。
    • しかし携帯IDは、すべてのウェブサイトに送信されるものですので、いわば公開情報です。このため、携帯IDを照合するだけでは、利用者を認証したことにはなりません。
    • かつて、携帯ウェブは次の2つの前提が成り立つと考えられていたことから、携帯IDを用いて利用者の認証が可能と考えられていました。
      • 1. ウェブサイトへのアクセスは、携帯ウェブのブラウザからのみ行われる。または、携帯ウェブのブラウザ以外からのアクセスをウェブサイト側で識別できる。
      • 2. 携帯ウェブのブラウザから、利用者による操作で、送信するHTTPリクエストのヘッダを任意に変更することができない。
    • しかし、近年、このような前提は実際には成り立たなくなってきました。
    • 上の前提を満たすために、キャリアが提供しているIPアドレスリストを使用し、アクセス元IPアドレスに基づく制限をする方法が、しばしば用いられます。
    • しかし、このようなリストは、キャリアが正しさを保証していなかったり、安全な取得方法が提供されていなかったり、更新のタイミングを適切に追うことができない等、様々な問題を抱えています。
    • その上、近年では一部のキャリアにおいて、PCを用いてそれらのIPアドレスからのアクセスが可能になっています。
    • 携帯IDによる利用者認証が安全であるためには、ウェブアプリケーションに届く携帯IDが偽装されないことが必要ですが、上記の通り、上の前提が崩れているため、あるキャリアでは端末に割り振られた携帯IDの偽装を回避できないこと、また、契約者に割り振られた携帯IDも、一部のキャリアではウェブアプリケーションの実装によっては偽装されてしまうことが知られています。
    • また、一般にスマートフォンでは簡単に偽装されてしまいます。
    • このように、携帯IDを用いて利用者を認証することは簡単ではありません。
    • パスワードやCookie等を使用した、PC向けサイトと同様の認証方式を採用するか、キャリアが提供する安全な認証方式を採用してください。
    • キャリアによって認定された、いわゆる公式サイトでは、キャリアから携帯IDの安全な使い方に関する情報の開示を受けられることがあります。
    • しかしそれ以外のサイトでは、その情報を得られないため、安全な使い方を把握できず、結果としてウェブサイトの安全性が損なわれる場合があります。
  • ◆認証情報に関する注意点
    • 秘密ではないものを認証情報として使用
      • 認証情報は、「パスワード」や「暗証番号」など、ウェブサイトと利用者の間だけで共有される秘密情報でなくてはなりません。生年月日などは本人以外も知っている可能性があるため、ダメ。
    • 認証強度が足りない場合
      • 認証情報が、第三者による推測や試行によって破られることがないよう、ウェブサイトは、利用者が十分に複雑な認証情報を使用できるようにする必要があります。
      • 携帯電話の入力インターフェースはPCと異なる方式で、長い文字列の入力には向いていません。
      • このため携帯電話向けウェブサイトにおいては、入力する認証情報を数字のみにするといった設計がなされがちです。しかし、数字のみによる認証は、簡単に破られてしまう場合があります。
      • ウェブでは多くの場合、試行回数を確実に制限することは困難です。
      • たとえば、試行回数を制限する単純な方法として、あるユーザIDに対するパスワードの間違いが一定回数を超えた場合には、アカウントをロックするといった対策が考えられますが、このような単純な対策では、パスワードを固定してユーザIDを変更する方式の試行(リバースブルートフォース)に効果がありません。
      • ウェブにおける利用者の認証では、認証情報だけが頼りになります。認証情報を数字だけに制限したりせず、英数字を織り交ぜた桁数の多いパスワードを使用できるようにしてください。
    • 利便性との両立
      • パターン数の多いパスワードは、利用者から見れば入力の手間を要するものです。このためウェブサイトを設計する際、利便性を優先してパスワードのパターン数を少なくする方向性の設計に傾くことがあるかもしれません。
      • しかし、利用者の安全性も考慮し、パターン数を確保したまま入力頻度を減らす設計を検討してください。
      • 入力頻度を減らす方法は幾つかありますが、代表的なものは、Cookie機能を用いて一定期間有効なセッションIDを発行し、そのセッションIDが有効な間は認証済みとみなす手法です。
      • PC向けのウェブサイトではしばしば、「次回から自動的にログイン」「ログイン状態を保持する」等の説明の下、利用者の選択でこの機能を使用できる仕組みが提供されています。
      • セッションIDの有効期間を長くするほど、パスワードの入力頻度を抑えることができます。具体的な有効期間はウェブサイトのサービス内容に応じて個別に検討してください。
    • パスワードに用いる文字の種類とパターン数の関係
      • PC向けのウェブサイトでは、英数字と記号文字を織り交ぜたパスワードを使用するよう、しばしば推奨されます。
      • このようなパスワードには高い強度がありますが、携帯電話で入力することは現実的ではない場合があります。
      • 携帯電話においては、数字のみをパスワードにすることが現実的な方法として考えられますが、その場合は桁数を多くして、十分なパターン数を確保する必要があります。

これらの内容をもとに「体系的に学ぶ 安全なWebアプリケーションの作り方 脆弱性が生まれる原理と対策の実践」を読んで、知識を定着させていきたいと思っています。


最後までお読み頂き、ありがとうございました。

【Rails】【test】fixturesファイルの読込に関してのまとめ

Railsでテストを実行する時に、fixturesが読み込まれる、読み込まれないのタイミングがあいまいだったので色々と実験して得られた結果をもとにまとめていきます。

Railsでテストを実行する方法

  • rubyコマンドでテストファイル名を直接指定して実行する
  • Rakeコマンドで実行する
rubyコマンドで直接実行
例)test/unit/user_test.rbを実行する
ruby test/unit/user_test.rb
rakeコマンドで実行
  • 複数のテストをまとめてテストする時のためのRakeタスクが存在します。
    • rake db:test:prepare
    • rake test
    • rake test:functionals
    • rake test:units
    • rake test:recent

テストデータの用意の仕方

require File.dirname(__FILE__) + '/../test_helper'

class UserTest < ActiveSupport::TestCase
  fixtures :users, :schools #users.ymlとschools.ymlが読み込まれます

  def test_get_active
  (略)
  end
end

rubyコマンドで実行したテストとrakeコマンドで実行した時の結果が違うんだけど…?

ここで躓く原因はrailsでのテストの挙動の理解とrakeコマンドへの理解が足りていないためだと思われます。

railsでのテストの挙動の理解
  • railsでテストを走らせる場合、テストDBを参照して行われます。
  • テストDBはrake db:test:prepareコマンドで、development環境のDBからスキーマをコピーして、生成されます。
  • それぞれのtableの中身のデータはtestファイル内にfixtures宣言を書いてあげることで読み込まれます。
  • さらに、rakeコマンドでテストを実行された場合、1度読み込まれたfixturesはテストDBにデータが入ったまま次のテストが実行されていきます。
  • よってテストファイルが読み込まれる順番が違うためにエラーが起きるといったこともありえます。
  • これをなくすためには、テストファイル内で扱うデータはfixtures宣言で再読みこみすることだと思います。
rakeコマンドへの理解
  • まず、rakeコマンドでどんなことが出来るか?それはrake --task(rake -T)で確認することができます。
rake db:fixtures:load          # Load fixtures into the current environment's database.
rake db:migrate                # Migrate the database through scripts in db/migrate.
rake db:schema:dump            # Create a db/schema.rb file that can be portably used against any DB supported by AR
rake db:schema:load            # Load a schema.rb file into the database
rake db:sessions:clear         # Clear the sessions table
rake db:sessions:create        # Creates a sessions table for use with CGI::Session::ActiveRecordStore
rake db:structure:dump         # Dump the database structure to a SQL file
rake db:test:clone             # Recreate the test database from the current environment's database schema
rake db:test:clone_structure   # Recreate the test databases from the development structure
rake db:test:prepare           # Prepare the test database and load the schema
rake db:test:purge             # Empty the test database
rake doc:app                   # Build the RDOC HTML Files
rake doc:clobber_app           # Remove rdoc products
rake doc:clobber_plugins       # Remove plugin documentation
rake doc:clobber_rails         # Remove rdoc products
rake doc:plugins               # Generate documation for all installed plugins
rake doc:rails                 # Build the RDOC HTML Files
rake doc:reapp                 # Force a rebuild of the RDOC files
rake doc:rerails               # Force a rebuild of the RDOC files
rake log:clear                 # Truncates all *.log files in log/ to zero bytes
rake rails:freeze:edge         # Lock to latest Edge Rails or a specific revision with REVISION=X (ex: REVISION=4021) or a tag with TAG=Y (ex: TAG=rel_1-1-0)
rake rails:freeze:gems         # Lock this application to the current gems (by unpacking them into vendor/rails)
rake rails:unfreeze            # Unlock this application from freeze of gems or edge and return to a fluid use of system gems
rake rails:update              # Update both configs, scripts and public/javascripts from Rails
rake rails:update:configs      # Update config/boot.rb from your current rails install
rake rails:update:javascripts  # Update your javascripts from your current rails install
rake rails:update:scripts      # Add new scripts to the application script/ directory
rake stats                     # Report code statistics (KLOCs, etc) from the application
rake test                      # Test all units and functionals
rake test:functionals          # Run tests for functionalsdb:test:prepare / Run the functional tests in test/functional
rake test:integration          # Run tests for integrationdb:test:prepare / Run the integration tests in test/integration
rake test:plugins              # Run tests for pluginsenvironment / Run the plugin tests in vendor/plugins/**/test (or specify with PLUGIN=name)
rake test:recent               # Run tests for recentdb:test:prepare / Test recent changes
rake test:uncommitted          # Run tests for uncommitteddb:test:prepare / Test changes since last checkin (only Subversion)
rake test:units                # Run tests for unitsdb:test:prepare / Run the unit tests in test/unit
rake tmp:cache:clear           # Clears all files and directories in tmp/cache
rake tmp:clear                 # Clear session, cache, and socket files from tmp/
rake tmp:create                # Creates tmp directories for sessions, cache, and sockets
rake tmp:pids:clear            # Clears all files in tmp/pids
rake tmp:sessions:clear        # Clears all files in tmp/sessions
rake tmp:sockets:clear         # Clears all files in tmp/sockets
  • 重要なのは、rake testrake test:functionalsrake test:unit
  • rake test:functionalsでは、rake db:test:prepareをした後に、test/functional以下に存在するファイルを実行すると書いてあります。
  • さらにrake test:unitsでも、rake db:test:prepareをした後に、test/unit以下に存在するファイルを実行すると書いてあります。
  • 要は、都度テストDBをinitializeしていることになります。
  • しかし、rake testを実行するとunitテストが走ってfunctionalテストが走るのですが、rake db:test:prepareが呼び出されるのは初めの1度のみです。
  • これもrubyコマンドでテストを実行した時とrakeコマンドでテストを実行した時の挙動の違いを生む原因かなと思います。
まとめ
  • rakeコマンドは自分が思っていたより多機能。もっと色々な機能があるっぽい。
  • rake doc:qppを実行した時に、なんとなくの仕様書がHTMLベースで生成できたことには感動しました。なんだかrakeコマンドで色んな事できそうで楽しそうです。

最後までお読み下さり、ありがとうございました。

Railsレシピブック 183の技

Railsレシピブック 183の技

【rails】alias_method_chainとwith_scopeに関して

Railsアプリケーションを作っていく時に使える、alias_method_chainとwith_scopeのスマートな合わせ技をご紹介します。
Railsは1系を利用しています。2系からはnamed_scopeが使えるので、そちらを使うべきかと。

データ構造
create_table :users do |t|
  t.column :name, :string
  t.column :status, :enum, :limit => [:active, :inactive], :default => :active
  t.column :age, :tinyint
  t.column :company_id, :integer
  t.column :created_at, :timestamp
  t.column :updated_at, :timestamp
end
create_table :companies do |t|
  t.column :name, :string
  t.column :created_at, :timestamp
  t.column :updated_at, :timestamp
end
  • ユーザー(users)テーブルには論理削除機能をつけるためにstatusカラムが存在している
  • user has_many companies


状況設定
  • ユーザーをfindする時は、基本的にststus => :activeなもの
  • ユーザーをカウントする時も、基本的にstatus => :activeなもの


毎回find,countする時に条件書くの面倒くさいな…
  • そんなときに今回ご紹介するalias_method_chainとwith_scopeを使います
class  User < ActiveRecord::Base
  has_many :companies

  class << self
    def find_with_active(*args)
      with_scope(:find => {:conditions =>["status = ?", :active]}) do
        find_without_active(*args)
      end
    end
    alias_method_chain :find, :active

    def count_with_active(*args)
      with_scope(:find=> {:conditions =>["status = ?", :active]}) do
        count_without_active(*args)
      end
    end 
    alias_method_chain :count, :active
  end

  def destroy
    update_attribute(:status, :inactive)
  end
end
  • alias_method_chain :find, :activeは、以下の宣言と同義です。
alias_method :find, :find_with_active
alias_method :find_without_active, :find  
  • すなわち、findメソッドでfind_with_activeメソッドが呼び出され、find_without_activeメソッドでfindメソッドが呼び出されることになります。
  • この2行をたった1行で表現してくれるのがalias_method_chain :find, :active


上のコードをもう少しスマートに書く
class << self
  def find_with_active(*args)
    actived_scope{find_without_active(*args)}
  end
  alias_method_chain :find, :active
 
  def count_with_active(*args)
    actived_scope{count_without_active(*args)}
  end
  alias_method_chain :count, :active
        
  def actived_scope
    scope_condition = {:conditions => ["working_hours.status = ?", :active]}
    
    with_scope(:find => scope_condition) do
      yield    # スマートポイント!
    end
  end
end  



ん?findでcondition指定しているのに、countって適用されるの?
  • それが適用されるのです!詳しくはソース見ると分かります。
def with_scope(method_scoping = {}, action = :merge, &block)
  method_scoping = method_scoping.method_scoping if method_scoping.respond_to?(:method_scoping)

  # Dup first and second level of hash (method and params).
  method_scoping = method_scoping.inject({}) do |hash, (method, params)| 
    hash[method] = (params == true) ? params : params.dup
    hash
  end

  method_scoping.assert_valid_keys([ :find, :create ])

  if f = method_scoping[:find]
    f.assert_valid_keys([ :conditions, :joins, :select, :include, :from, :offset, :limit, :order, :readonly, :lock ]) #ココが重要!
    f[:readonly] = true if !f[:joins].blank? && !f.has_key?(:readonly)
  end

  # Merge scopings
  if action == :merge && current_scoped_methods
    method_scoping = current_scoped_methods.inject(method_scoping) do |hash, (method, params)|
      case hash[method]
        when Hash
          if method == :find
            (hash[method].keys + params.keys).uniq.each do |key|
              merge = hash[method][key] && params[key] # merge if both scopes have the same key
              if key == :conditions && merge
                hash[method][key] = [params[key], hash[method][key]].collect{ |sql| "( %s )" % sanitize_sql(sql) }.join(" AND ")
              elsif key == :include && merge
                hash[method][key] = merge_includes(hash[method][key], params[key]).uniq
              else
                hash[method][key] = hash[method][key] || params[key]
              end
            end
          else
            hash[method] = params.merge(hash[method])
          end
        else
          hash[method] = params
      end
      hash
    end
  end
                   
  self.scoped_methods << method_scoping
 
  begin 
    yield
  ensure    
    self.scoped_methods.pop
  end
end
  • 要はcountメソッドに{:conditions => ["users.status = ?", :active]}が追加されただけなんです。
  • 「ココが重要!」と書いた行にあるオプションに対して条件を付けることができます。
  • orderなんかも使い所多そうですね。defaultで降順に並び変えたい時とか。
  • あとはうまくやればページネーションも実装できるかも。今度試してみます。


with_scopeはActiveRecordの色んなところで活躍している


結論
  • with_scopeもalias_method_chainもとっても便利
  • ActiveRecordを学習するのはとっても楽しい!これからも学習を続けましょうということです。