The OWASP description - Many web applications do not properly protect sensitive data, such as credit cards, SSNs, and authentication credentials, with appropriate encryption or hashing. Attackers may steal or modify such weakly protected data to conduct identity theft, credit card fraud, or other crimes.
Railsgoat does hash user passwords. Unfortunately, it does so using an extremely weak algorithm (MD5). Generally speaking, a strong algorithm and per-user salt can greatly improve the security of a hashed value. Also important to note, hashing and encryption are not the same. Encryption is meant to be reversible using some secret information, hashing is not, hashing is a one-way function not meant to be reversible.
All that being said, there are groups within security organizations that devote themselves to threat models built around this topic so clearly, this description does not encompass all scenarios. However, our recommendation is better than hashing using MD5 .
Within app/models/user.rb:
before_save :hash_password def self.authenticate(email, password) auth = nil user = find_by_email(email) if user if user.password == Digest::MD5.hexdigest(password) auth = user else raise "Incorrect Password!" end else raise "#{email} doesn't exist!" end return auth end def hash_password if self.password.present? self.password = Digest::MD5.hexdigest(password) end end
Password Storage - ATTACK
Using the passwords stored within db/seeds.rb file, create a wordlist and leverage a password cracking tool such as John The Ripper to crack those passwords.
Password Storage - SOLUTION
A simple solution here would be to enforce a per-user salt in creating a BCrypt hash. You would need to alter the db schema to add a password_salt and password_hash columns to the table.
def self.authenticate(email, password) user = find_by_email(email) if user and user.password_hash == BCrypt::Engine.hash_secret(password, user.password_salt) user else "Invalid Credentials Supplied" end end def hash_password if self.password.present? self.password_salt = BCrypt::Engine.generate_salt self.password_hash = BCrypt::Engine.hash_secret(self.password, self.password_salt) end end
The Railsgoat application stores and transmits Social Security Numbers insecurely.
The Railsgoat application stores user's Social Security Numbers in plain-text within the database and because of this, it fails to adequately protect these numbers from theft. Additionally, the user's full SSN is sent back to the user within an HTTP response from the application.
The WorkInfo model (app/models/work_info.rb) is missing code to encrypt this data prior to storage. Additionally, while code exists to render only the last 4 numbers of an SSN (shown below), at no time is it used.
# We should probably use this def last_four "***-**-" << self.decrypt_ssn[-4,4] end
SSN Storage - SOLUTION
There is a lot of guidance on adequately protecting sensitive data at rest and using a layered defensive approach. Make no mistake, this should not be your sole means of securing sensitive data. That being said, there are at least four precautions that should be taken.
In the following code, we demonstrate switching from the storage of full SSN(s) in clear-text to storing them in the AES-256 encrypted format. The first thing to do is build the encrypt and decrypt functions. These can be found within app/models/work_info.rb.
def encrypt_ssn aes = OpenSSL::Cipher::Cipher.new(cipher_type) aes.encrypt aes.key = key aes.iv = iv if iv != nil self.encrypted_ssn = aes.update(self.SSN) + aes.final self.SSN = nil end def decrypt_ssn aes = OpenSSL::Cipher::Cipher.new(cipher_type) aes.decrypt aes.key = key aes.iv = iv if iv != nil aes.update(self.encrypted_ssn) + aes.final end def key raise "Key Missing" if !(KEY) KEY end def iv raise "No IV for this User" if !(self.key_management.iv) self.key_management.iv end def cipher_type 'aes-256-cbc' end
Also within the WorkInfo model, we add the following line of code...
before_save :encrypt_ssn
The remaining pieces are:
# SEED DATA work_info.each do |wi| list = [:user_id, :SSN] info = WorkInfo.new(wi.reject {|k| list.include?(k)}) info.user_id = wi[:user_id] info.build_key_management({:user_id => wi[:user_id], :iv => SecureRandom.hex(32) }) info.SSN = wi[:SSN] info.save end
# SEPARATE PROD AND DEV KEYS (config/initializers/key.rb) if Rails.env.production? # Specify env variable/location/etc. to retrieve key from elsif Rails.env.development? KEY = "123456789101112123456789101112123456789101112" end
# CHANGE VIEW TO CALL LAST FOUR METHOD (app/views/work_info/index.html.erb) <td class="ssn"><%= @user.work_info.last_four %></td>
def build_benefits_data build_retirement(POPULATE_RETIREMENTS.shuffle.first) build_paid_time_off(POPULATE_PAID_TIME_OFF.shuffle.first).schedule.build(POPULATE_SCHEDULE.shuffle.first) build_work_info(POPULATE_WORK_INFO.shuffle.first) # Uncomment below line to use encrypted SSN(s) work_info.build_key_management(:iv => SecureRandom.hex(32)) performance.build(POPULATE_PERFORMANCE.shuffle.first) end
The application's API returns a model object (user or users). Using respond_with, the API returns the full model object. It is simple but exposes information such as the user's password and other user attributes that you may wish to keep invisible.
Within app/controllers/api/v1/users_controller.rb:
def index # We removed the .as_json code from the model, just seemed like extra work. # dunno, maybe useful at a later time? #respond_with @user.admin ? User.all.as_json : @user.as_json respond_with @user.admin ? User.all : @user end def show respond_with @user.as_json end
The as_json method referenced in the comments section of the index action exists within the user model in order to override and safely protect our model from only rendering certain attributes. It is unused (commented out), app/models/user.rb:
# Instead of the entire user object being returned, we can use this to filter. def as_json super(only: [:user_id, :email, :first_name, :last_name]) end
When utilizing the method that most tutorials describe or advocate when rendering model objects via JSON in an API (unsafe), the response looks like this:
HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8 X-UA-Compatible: IE=Edge ETag: "6b4caf343a20865de174b2b530b945dd" Cache-Control: max-age=0, private, must-revalidate X-Request-Id: c3b0a57861087c0b827aab231747ef0c X-Runtime: 0.051734 Connection: close {"admin":false,"created_at":"2014-01-23T16:17:10Z","email": "[email protected]","first_name":"Jack","id":2,"last_name":"Mannino","password": "b46dd2888a0904972649cc880a93f4dd","updated_at":"2014-01-23T16:17:10Z","user_id":2}
Note that all attributes associated with this user are returned via the API.
Model Attributes Exposure - ATTACK
Use the API and review the data returned. Additional information on exploiting the API available under the Extras > Logic Flaws Section.
Model Attributes Exposure - SOLUTION
Uncomment the as_json method within the user model. Additionally, call .as_json on any User model object you would like to return via the API or other means. Example:
respond_with @user.admin ? User.all.as_json : @user.as_json
Upon uncommenting the as_json method within the User model, the as_json method will ensure the API output only returns those attributes you have allowed in the following code:
def as_json
super(only: [:user_id, :email, :first_name, :last_name])
end
The response from the API should look like:
HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8 X-UA-Compatible: IE=Edge ETag: "2333488e856669ac637e37cb4cf09cb6" Cache-Control: max-age=0, private, must-revalidate X-Request-Id: baa6a1c90004838793614e4c61633767 X-Runtime: 0.092768 Connection: close {"email":"[email protected]","first_name":"Jack","last_name":"Mannino","user_id":2}