Backup codes are currently stored as plain JSON in the backup_codes column in the database:
# app/models/user.rbdefverify_backup_code(code)codes=JSON.parse(backup_codes)# Plain text codesifcodes.include?(code)codes.delete(code)update(backup_codes:codes.to_json)trueendend
The Problem
✗ Anyone with database access can read all backup codes
✗ Backup codes visible from Rails console
✗ Database compromise exposes 2FA bypass codes
✗ Violates principle of treating auth credentials like passwords
Industry Best Practice
Backup codes should be hashed (like passwords) and only shown once during generation:
Services that hash backup codes:
GitHub - shows once, then hashes
Google - shows once, then hashes
Most enterprise SSO providers
Recommended Approach:
Generate backup codes and show them once to the user
Hash each code with bcrypt before storing
On verification, hash the entered code and compare
Remove the matching hash from the array
# Pseudocode for improved implementationdefverify_backup_code(code)returnfalseunlessbackup_codes.present?hashed_codes=JSON.parse(backup_codes)# Find matching hashmatching_hash=hashed_codes.finddo|hashed|BCrypt::Password.new(hashed)==codeendifmatching_hashhashed_codes.delete(matching_hash)update(backup_codes:hashed_codes.to_json)trueelsefalseendend
Trade-offs
Current approach (plain text):
✓ Can re-display backup codes to user (with password verification)
✗ Security risk if database compromised
Hashed approach:
✓ Secure - codes invisible even with database access
✓ Industry standard practice
✗ Cannot re-display codes to user
Note: Most services consider this trade-off acceptable - backup codes are shown once with strong warnings to save them securely.
Proposed Changes
Update User#verify_backup_code to hash and compare codes
Update backup code generation to hash before storing
Remove "View Backup Codes" feature (or only allow during initial setup)
Update backup codes view to emphasize "save these now - you won't see them again"
Consider adding "regenerate backup codes" feature (invalidates old ones, generates new set)
Medium - This is a security improvement but requires database access to exploit. Should be addressed before production deployment or when handling sensitive data.
## Security Issue: Plain Text Backup Code Storage
### Current Implementation
Backup codes are currently stored as plain JSON in the `backup_codes` column in the database:
```ruby
# app/models/user.rb
def verify_backup_code(code)
codes = JSON.parse(backup_codes) # Plain text codes
if codes.include?(code)
codes.delete(code)
update(backup_codes: codes.to_json)
true
end
end
```
### The Problem
- ✗ Anyone with database access can read all backup codes
- ✗ Backup codes visible from Rails console
- ✗ Database compromise exposes 2FA bypass codes
- ✗ Violates principle of treating auth credentials like passwords
### Industry Best Practice
Backup codes should be **hashed** (like passwords) and only shown once during generation:
**Services that hash backup codes:**
- GitHub - shows once, then hashes
- Google - shows once, then hashes
- Most enterprise SSO providers
**Recommended Approach:**
1. Generate backup codes and show them once to the user
2. Hash each code with bcrypt before storing
3. On verification, hash the entered code and compare
4. Remove the matching hash from the array
```ruby
# Pseudocode for improved implementation
def verify_backup_code(code)
return false unless backup_codes.present?
hashed_codes = JSON.parse(backup_codes)
# Find matching hash
matching_hash = hashed_codes.find do |hashed|
BCrypt::Password.new(hashed) == code
end
if matching_hash
hashed_codes.delete(matching_hash)
update(backup_codes: hashed_codes.to_json)
true
else
false
end
end
```
### Trade-offs
**Current approach (plain text):**
- ✓ Can re-display backup codes to user (with password verification)
- ✗ Security risk if database compromised
**Hashed approach:**
- ✓ Secure - codes invisible even with database access
- ✓ Industry standard practice
- ✗ Cannot re-display codes to user
**Note:** Most services consider this trade-off acceptable - backup codes are shown once with strong warnings to save them securely.
### Proposed Changes
1. Update `User#verify_backup_code` to hash and compare codes
2. Update backup code generation to hash before storing
3. Remove "View Backup Codes" feature (or only allow during initial setup)
4. Update backup codes view to emphasize "save these now - you won't see them again"
5. Consider adding "regenerate backup codes" feature (invalidates old ones, generates new set)
### References
- [OWASP: Password Storage Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html)
- GitHub's 2FA implementation shows codes once only
- Google's backup codes are one-time view only
### Priority
**Medium** - This is a security improvement but requires database access to exploit. Should be addressed before production deployment or when handling sensitive data.
---
Related to: #1 (Development Progress Tracker)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Security Issue: Plain Text Backup Code Storage
Current Implementation
Backup codes are currently stored as plain JSON in the
backup_codescolumn in the database:The Problem
Industry Best Practice
Backup codes should be hashed (like passwords) and only shown once during generation:
Services that hash backup codes:
Recommended Approach:
Trade-offs
Current approach (plain text):
Hashed approach:
Note: Most services consider this trade-off acceptable - backup codes are shown once with strong warnings to save them securely.
Proposed Changes
User#verify_backup_codeto hash and compare codesReferences
Priority
Medium - This is a security improvement but requires database access to exploit. Should be addressed before production deployment or when handling sensitive data.
Related to: #1 (Development Progress Tracker)
complete