Login Form from Scratch.

Login Form from Scratch.

At Tokyu RubyKaigi 06 on June 29, 2012

Cf7b553387b247d737c60cfceabb2cea?s=128

Naoto Takai

June 29, 2013
Tweet

Transcript

  1. Login Form from Scratch @takai   

  2. Naoto Takai Cowboy Coder

  3. How many people have developed a login form?

  4. def create @login = Login.new(login_params) digest = Digest::SHA1.hexdigest(@login.password) if (user

    = User.find_by(email: @login.email, password_digest: digest)) session[:user] = user.id redirect_to @login.original_url || root_url else @login.errors[:base] << 'Please enter a correct username and password.' render :new end end
  5.  There are many, many pitfalls. You’re right, previous code

    is completely wrong.
  6. def create @login = Login.new(login_params) digest = Digest::SHA1.hexdigest(@login.password) if (user

    = User.find_by(email: @login.email, password_digest: digest)) session[:user] = user.id redirect_to @login.original_url || root_url else @login.errors[:base] << 'Please enter a correct username and password.' render :new end end
  7. Use PBKDF2-HMAC-SHA512 instead of SHA1 or MD5.

  8. NUM_OF_ITERATIONS = 20000 DIGEST_ALGORITHM = OpenSSL::Digest::SHA512.new KEY_LEN = DIGEST_ALGORITHM.length salt

    = SecureRandom.hex(32) OpenSSL::PKCS5.pbkdf2_hmac(password, salt, NUM_OF_ITERATIONS, KEY_LEN, DIGEST_ALGORITHM) # => "L\xBEv-\x83\xC2\x98\x1D..."  A minimum of 1,000 iterations is recommended in RFC 2898 (over ten years ago).  Use random and different salt for every password.
  9. def create @login = Login.new(login_params) user = User.find_by(email: @login.email) ||

    User.new(salt: '') digest_bytes = OpenSSL::PKCS5.pbkdf2_hmac(@login.password, user.salt, NUM_OF_ITERATIONS, KEY_LEN, DIGEST_ALGORITHM) password_digest = digest_bytes.unpack('H*').first if user.password_digest == password_digest session[:user] = user.id redirect_to @login.original_url || root_url ...
  10. def create @login = Login.new(login_params) user = User.find_by(email: @login.email) ||

    User.new(salt: '') digest_bytes = OpenSSL::PKCS5.pbkdf2_hmac(@login.password, user.salt, NUM_OF_ITERATIONS, KEY_LEN, DIGEST_ALGORITHM) password_digest = digest_bytes.unpack('H*').first if user.password_digest == password_digest session[:user] = user.id redirect_to @login.original_url || root_url ...
  11. Use secure_compare instead of == to avoid a timing attack.

     Timing attacks would not be viable in our case.
  12. module Rack module Utils # Constant time string comparison. def

    secure_compare(a, b) return false unless bytesize(a) == bytesize(b) l = a.unpack("C*") r, i = 0, -1 b.each_byte { |v| r |= v ^ l[i+=1] } r == 0 end module_function :secure_compare  https://github.com/rack/rack/blob/rack-1.5/lib/rack/utils.rb#L398-L408
  13. lhs = '7319bf2143beb945070723bf54965b2d' lhs2 = '7319bf2143beb945070723bf54965b2d' rhs = '1e64ac58521914da0e3804326e8a8a75' include

    Rack::Utils Benchmark.bm(12) do |x| x.report('#1 == (diff)') { i.times { lhs == rhs }} x.report('#2 == (same)') { i.times { lhs == lhs2 }} x.report('#3 sc (diff)') { i.times { secure_compare(lhs, rhs) }} x.report('#4 sc (same)') { i.times { secure_compare(lhs, lhs2) }} end __END__ user system total real #1 == (diff) 0.080000 0.000000 0.080000 ( 0.080617) #2 == (same) 0.090000 0.000000 0.090000 ( 0.085593) #3 sc (diff) 6.520000 0.070000 6.590000 ( 6.587122) #4 sc (same) 6.500000 0.060000 6.560000 ( 6.561539)
  14. ... digest_bytes = OpenSSL::PKCS5.pbkdf2_hmac(@login.password, user.salt, NUM_OF_ITERATIONS, KEY_LEN, DIGEST_ALGORITHM) password_digest =

    digest_bytes.unpack('H*').first if Rack::Utils.secure_compare(user.password_digest, password_digest) session[:user] = user.id redirect_to @login.original_url || root_url else @login.errors[:base] << 'Please enter a correct username and password.' ...
  15. ... digest_bytes = OpenSSL::PKCS5.pbkdf2_hmac(@login.password, user.salt, NUM_OF_ITERATIONS, KEY_LEN, DIGEST_ALGORITHM) password_digest =

    digest_bytes.unpack('H*').first if Rack::Utils.secure_compare(user.password_digest, password_digest) session[:user] = user.id redirect_to @login.original_url || root_url else @login.errors[:base] << 'Please enter a correct username and password.' ...
  16. Reset session before login to avoid a session xation attack

  17. Attacker Victim Web App issues a session id feeds the

    session id accesses with the session id logs in
  18. ... password_digest = digest_bytes.unpack('H*').first if Rack::Utils.secure_compare(user.password_digest, password_digest) reset_session session[:user] =

    user.id redirect_to @login.original_url || root_url else @login.errors[:base] << 'Please enter a correct username and password.' render :new end end ...
  19. ... password_digest = digest_bytes.unpack('H*').first if Rack::Utils.secure_compare(user.password_digest, password_digest) reset_session session[:user] =

    user.id redirect_to @login.original_url || root_url else @login.errors[:base] << 'Please enter a correct username and password.' render :new end end ...
  20. Do not share session cookies between HTTP and HTTPS

  21. Attacker Victim Secure zone data sensitive data sni cookies session:

    e8a744 session: e8a744 spoof session: e8a744
  22. Attacker Victim Secure zone data sensitive data sni cookies session:

    71fc6e session: e8a744 spoof session: e8a744
  23. def set_login_cookies(user_id) key = SecureRandom.uuid secure_key = SecureRandom.uuid SessionStorage.set(key, user_id)

    SessionStorage.set(secure_key, user_id) cookies.signed[:user_session] = { expires: 1.week.from_now, value: key, httponly: true } cookies.signed[:secure_user_session] = { expires: 1.week.from_now, value: secure_key, httponly: true, secure: true } end
  24. ... password_digest = digest_bytes.unpack('H*').first if Rack::Utils.secure_compare(user.password_digest, password_digest) reset_session set_login_cookies(user.id) redirect_to

    @login.original_url || root_url else @login.errors[:base] << 'Please enter a correct username and password.' render :new end end ...
  25. ... password_digest = digest_bytes.unpack('H*').first if Rack::Utils.secure_compare(user.password_digest, password_digest) reset_session set_login_cookies(user.id) redirect_to

    @login.original_url || root_url else @login.errors[:base] << 'Please enter a correct username and password.' render :new end end ...
  26. Avoid open redirect issue with whitelist

  27. Attacker Victim Attacker's site login form phishing page malicious url

    logs in redirection
  28. ... set_login_cookies(user.id) redirect_to sanitize_url(@login.original_url) || root_url else @login.errors[:base] << 'Please

    enter a correct username and password.' render :new end end ... def sanitize_url(url) URI.parse(url).host == request.host ? url : nil rescue nil end
  29. BONUS: turn off autocomplete

  30. == form_for :login, url: login_path do |f| .row .large-4.columns ==

    f.label :email == f.text_field :email, autocomplete: 'off' .row .large-4.columns == f.label :password == f.password_field :password .row .large-4.columns == f.hidden_field :original_url == f.submit Login, class: 'button'
  31. Thank you. @takai