RailsでのCSRF token 発行 / 検証のロジック
RailsでのCSRF対策
RailsではdefaultでFormHelperやjquery-railsによりauthenticity token
をリクエストに追加して送り、サーバーで検証するというCSRF対策がされています。rails csrf
と検索すると関連するページがたくさん見つかります。
今回は普段は気にしなくても良い、token発行や検証の仕組みを確認してみました。
ソースコード
ロジックはすべてAction Packのrequest_forgery_protection.rbに実装されていて、実際にコードを確認しました。
Authenticity Token 発行
まずはToken発行の仕組みを見ていきます。
form_authenticity_token
というhelper methodがviewからアクセスできるようになっており、tokenを発行するインターフェースとなっており、その中で呼ばれているmasked_authenticity_token
でtokenの発行ロジックが記述されています。
def masked_authenticity_token(session, form_options: {}) # :doc: action, method = form_options.values_at(:action, :method) raw_token = if per_form_csrf_tokens && action && method action_path = normalize_action_path(action) per_form_csrf_token(session, action_path, method) else real_csrf_token(session) end one_time_pad = SecureRandom.random_bytes(AUTHENTICITY_TOKEN_LENGTH) encrypted_csrf_token = xor_byte_strings(one_time_pad, raw_token) masked_token = one_time_pad + encrypted_csrf_token Base64.strict_encode64(masked_token) end
raw_token
... raw_token = if per_form_csrf_tokens && action && method action_path = normalize_action_path(action) per_form_csrf_token(session, action_path, method) else ...
per_form_csrf_token
を見ると、セッションに保存されたcsrf tokenとaction, method を組み合わせてSHA256 ハッシュを作成しています。これが raw_token
として使われています。
session[:_csrf_token]
def real_csrf_token(session) # :doc: session[:_csrf_token] ||= SecureRandom.base64(AUTHENTICITY_TOKEN_LENGTH) Base64.strict_decode64(session[:_csrf_token]) end
raw_token
の計算に使われるsassion[:_csrf_token]
はSecureRandom.base64
で作られたランダムな文字列が保存されていて、同じものを同じセッションで使い回されていてます。
masked_token
... one_time_pad = SecureRandom.random_bytes(AUTHENTICITY_TOKEN_LENGTH) encrypted_csrf_token = xor_byte_strings(one_time_pad, raw_token) masked_token = one_time_pad + encrypted_csrf_token ...
生成されたraw_token
とone_time_pad
を合わせてmasked_token
が生成されています。
one_time_pad
はraw_token
と同じ長さのランダムなバイト列で、xor_byte_strings
というmethodでraw_token
にマスクするのに使われています。このmethodは検証にも使われます。
authenticity token
...
Base64.strict_encode64(masked_token)
end
masked_token
をbase64でencodeしたものがauthenticity token
として、HTMLページに埋め込まれます。
Authenticity Token 検証
次にToken検証のロジックを見ていきます。
全てのリクエストはbefore_action
でverify_authenticity_token
が実行されていて、authenticity token
の検証が必要なリクエストに対して検証ロジックが実行されます。
ロジック自体はvalid_authenticity_token?
に記述されています。
def valid_authenticity_token?(session, encoded_masked_token) # :doc: ... begin masked_token = Base64.strict_decode64(encoded_masked_token) rescue ArgumentError # encoded_masked_token is invalid Base64 return false end if masked_token.length == AUTHENTICITY_TOKEN_LENGTH compare_with_real_token masked_token, session elsif masked_token.length == AUTHENTICITY_TOKEN_LENGTH * 2 csrf_token = unmask_token(masked_token) compare_with_real_token(csrf_token, session) || valid_per_form_csrf_token?(csrf_token, session) else false # Token is malformed. end end
decode token
masked_token = Base64.strict_decode64(encoded_masked_token)
まずはリクエストで送られてきたtokenをBase64でdecodeします
unmasked tokenの場合
if masked_token.length == AUTHENTICITY_TOKEN_LENGTH compare_with_real_token masked_token, session elsif masked_token.length == AUTHENTICITY_TOKEN_LENGTH * 2 ...
masked_token
という変数名ですが、長さがAUTHENTICITY_TOKEN_LENGTH
と同じ場合はmaskされていないものとして、セッション内に保存されたcsrf tokenと直接比較しています。
masked tokenの場合
elsif masked_token.length == AUTHENTICITY_TOKEN_LENGTH * 2 csrf_token = unmask_token(masked_token) compare_with_real_token(csrf_token, session) || valid_per_form_csrf_token?(csrf_token, session) else ...
変数masked_token
の長さがAUTHENTICITY_TOKEN_LENGTH
の2倍の場合は、maskされているものとして、unmask_token
をしてから、compare_with_real_token
実行されています。
unmask_token
def unmask_token(masked_token) # :doc: # Split the token into the one-time pad and the encrypted # value and decrypt it. one_time_pad = masked_token[0...AUTHENTICITY_TOKEN_LENGTH] encrypted_csrf_token = masked_token[AUTHENTICITY_TOKEN_LENGTH..-1] xor_byte_strings(one_time_pad, encrypted_csrf_token) end
コメントにあるように、tokenを半分に分割します。
ここでxor_byte_strings
にone_time_pad
とencrypted_csrf_token
を実行することで、encrypted_csrf_token
のmaskを外しています。
maskが外されたcsrf_token
とセッション内に保存されたcsrf tokenを比較して一致するか比較しています。
Demo
ロジックがわかれば、HTMLページ内のauthenticity_token
からセッションに保存されている CSRF tokenが計算することができます。
https://gist.github.com/Jwata/4e5122fa43d719400914716955872cc2
> git clone https://gist.github.com/Jwata/4e5122fa43d719400914716955872cc2 authenticity_token_to_csrf_token > rails runner ./authenticity_token_to_csrf_token/calculate.rb 'S5O27L72mhZRQk4rP16sIbj2m0BI7SrujJyh+hEXoaeTaT6GHxXx6d7UFnU05z6VxWFgb4wG+AtFsuHb5mqQ/A==' => 2PqIaqHja/+PllheC7mStH2X+y/E69LlyS5AIfd9MVs= > rails runner ./authenticity_token_to_csrf_token/calculate.rb 'JU6qRx/avhFPb/8+F3oZl/Iabr3nSbWVIgSHPm0Bz9n9tCItvjnV7sD5p2Acw4sjj42VkiOiZ3DrKscfmnz+gg==' => 2PqIaqHja/+PllheC7mStH2X+y/E69LlyS5AIfd9MVs=
この場合は2PqIaqHja/+PllheC7mStH2X+y/E69LlyS5AIfd9MVs=
がsession[:_csrf_token]
の値です。