Created
March 15, 2017 06:15
-
-
Save shotat/b967b015c72d0b4dd7f86e773117b8cc to your computer and use it in GitHub Desktop.
Railsのcsrf対策処理のencodingと検証処理をざっくり
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require 'securerandom' | |
require 'base64' | |
# https://github.com/rails/rails/blob/master/actionview/lib/action_view/helpers/csrf_helper.rb#L20-L27 | |
# token = 'jYZcRG05o2IRERhLVqwNVZREdUe4XB0okJRcBUrBQlQ=' | |
# <meta name="csrf-param" content="authenticity_token" /> | |
# <meta name="csrf-token" content="sZu120+MWcK2pEomGML/5SsRSgmGaYOwt0ABKTu663MV0FPC2ssoQuhRl2FXRAn+jEreDewPRIq0ZRh4AoTfxA==" /> | |
class Csrf | |
AUTHENTICITY_TOKEN_LENGTH = 32 | |
attr_accessor :session | |
def initialize(session) | |
self.session = session | |
end | |
# viewに埋め込むtokenのkey | |
def request_forgery_protection_token | |
:authenticity_token | |
end | |
# viewに埋め込むtoken | |
def form_authenticity_token | |
masked_authenticity_token(session) | |
end | |
# GET, HEADの時はチェックしない | |
# bodyの'authenticity_token'かheaderの'X-CSRF-Token'を読む | |
def verified_request?(encoded_masked_token) | |
# request.get? || request.head? || | |
# valid_authenticity_token?(session, form_authenticity_param) || | |
# valid_authenticity_token?(session, request.headers['X-CSRF-Token']) | |
valid_authenticity_token?(session, encoded_masked_token) | |
end | |
def valid_authenticity_token?(session, encoded_masked_token) # :doc: | |
if encoded_masked_token.nil? || encoded_masked_token.empty? || !encoded_masked_token.is_a?(String) | |
return false | |
end | |
begin | |
masked_token = Base64.strict_decode64(encoded_masked_token) | |
rescue ArgumentError # encoded_masked_token is invalid Base64 | |
return false | |
end | |
# See if it's actually a masked token or not. In order to | |
# deploy this code, we should be able to handle any unmasked | |
# tokens that we've issued without error. | |
if masked_token.length == AUTHENTICITY_TOKEN_LENGTH | |
# This is actually an unmasked token. This is expected if | |
# you have just upgraded to masked tokens, but should stop | |
# happening shortly after installing this gem. | |
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) | |
else | |
false # Token is malformed. | |
end | |
end | |
private | |
def compare_with_real_token(token, session) # :doc: | |
secure_compare(token, real_csrf_token(session)) | |
end | |
def secure_compare(a, b) | |
return false unless a.bytesize == b.bytesize | |
l = a.unpack "C#{a.bytesize}" | |
res = 0 | |
b.each_byte { |byte| res |= byte ^ l.shift } | |
res == 0 | |
end | |
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 | |
def masked_authenticity_token(session) # :doc: | |
raw_token = real_csrf_token(session) | |
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 | |
def xor_byte_strings(s1, s2) | |
s1.bytes.zip(s2.bytes).map { |(c1, c2)| c1 ^ c2 }.pack('c*') | |
end | |
def real_csrf_token(session) # :doc: | |
session[:_csrf_token] ||= SecureRandom.base64(AUTHENTICITY_TOKEN_LENGTH) | |
Base64.strict_decode64(session[:_csrf_token]) | |
end | |
end | |
token = 'jYZcRG05o2IRERhLVqwNVZREdUe4XB0okJRcBUrBQlQ=' | |
session = { _csrf_token: token } | |
csrf = Csrf.new(session) | |
encoded_masked_token = csrf.form_authenticity_token | |
p encoded_masked_token | |
#=> "WKsUsDYvg3ao2ekV6n2ecpWEsG9GF0gHJp0LSsWj7vfVLUj0WxYgFLnI8V680ZMnAcDFKP5LVS+2CVdPj2Ksow==" | |
invalid_encoded_masked_token = 'a' * encoded_masked_token.length | |
p invalid_encoded_masked_token | |
#=> "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" | |
p csrf.verified_request?(encoded_masked_token) | |
#=> true | |
p csrf.verified_request?(invalid_encoded_masked_token) | |
#=> false |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment