
Secure Password Storage in Bolt: Implementation Guide
Introduction
In the rapidly evolving landscape of vibe coding, Bolt has emerged as a powerful platform for quickly building functional applications with minimal technical expertise. However, this ease of development often comes with hidden security risks, particularly when it comes to password storage. According to recent security audits, over 78% of vibe-coded applications contain critical vulnerabilities in their password storage implementations, making them prime targets for attackers.
When developers instruct Bolt to “create a user authentication system” or “add login functionality,” the resulting code often prioritizes functionality over security. This leads to insecure password storage practices that can compromise user data and potentially your entire application. In this guide, we’ll examine common password storage vulnerabilities in Bolt applications and provide secure implementation patterns to prevent these vulnerabilities.
Common Password Storage Vulnerabilities in Bolt
1. Plain Text Password Storage
The most dangerous yet surprisingly common vulnerability in Bolt-generated applications is storing passwords in plain text:
// Vulnerable Bolt-generated code
const User = mongoose.model('User', new mongoose.Schema({
email: { type: String, required: true, unique: true },
password: { type: String, required: true }, // Stored as plain text
name: String,
createdAt: { type: Date, default: Date.now }
}));
// Registration handler
app.post('/register', async (req, res) => {
try {
const { email, password, name } = req.body;
// Create new user with plain text password
const user = new User({
email,
password, // Plain text password
name
});
await user.save();
res.status(201).json({ message: 'User created successfully' });
} catch (error) {
res.status(500).json({ message: 'Server error' });
}
});
This approach is catastrophic for security because:
- If your database is compromised, attackers can immediately access all user passwords
- Passwords are visible to anyone with database access, including developers and administrators
- It violates data protection regulations like GDPR and CCPA
2. Weak Hashing Algorithms
Another common vulnerability is using outdated or weak hashing algorithms:
// Vulnerable Bolt-generated code with weak hashing
const crypto = require('crypto');
function hashPassword(password) {
// Using MD5 (extremely weak) or SHA-1 (broken)
return crypto.createHash('md5').update(password).digest('hex');
}
app.post('/register', async (req, res) => {
try {
const { email, password, name } = req.body;
// Create new user with weakly hashed password
const user = new User({
email,
password: hashPassword(password), // Weak hashing
name
});
await user.save();
res.status(201).json({ message: 'User created successfully' });
} catch (error) {
res.status(500).json({ message: 'Server error' });
}
});
This approach is problematic because:
- Algorithms like MD5 and SHA-1 are cryptographically broken
- They’re designed for speed, not security, making brute force attacks feasible
- They lack built-in salting, making them vulnerable to rainbow table attacks
- They don’t have adjustable work factors to keep pace with hardware improvements
3. Missing Salt or Using Static Salt
Even when Bolt generates code with hashing, it often misses the critical step of using unique salts for each password:
// Vulnerable Bolt-generated code with static salt
const crypto = require('crypto');
const STATIC_SALT = 'bolt-app-salt'; // Same salt for all passwords
function hashPassword(password) {
// Using static salt
return crypto.pbkdf2Sync(password, STATIC_SALT, 1000, 64, 'sha512').toString('hex');
}
// Registration handler
app.post('/register', async (req, res) => {
try {
const { email, password, name } = req.body;
// Create new user with statically salted password
const user = new User({
email,
password: hashPassword(password), // Static salt
name
});
await user.save();
res.status(201).json({ message: 'User created successfully' });
} catch (error) {
res.status(500).json({ message: 'Server error' });
}
});
This approach is flawed because:
- Using the same salt for all passwords makes them vulnerable to rainbow table attacks
- If one password is compromised, the same technique can be applied to all passwords
- It doesn’t protect against precomputed hash attacks
Prompts to Generate Secure Bolt Code
Let’s explore specific prompts you can give Bolt to generate secure code that prevents password storage vulnerabilities.
Prompt 1: Implementing Secure Password Hashing with Argon2
Instead of asking: “Create a user authentication system with login and registration”
Use this prompt: “Create a secure user authentication system using Argon2 for password hashing. Implement a registration endpoint that generates a unique cryptographic salt for each user, hashes passwords with Argon2id using recommended memory, iterations, and parallelism parameters, and stores only the hash and salt in the database. Also implement a secure login endpoint that verifies passwords without exposing timing vulnerabilities. Include proper error handling and input validation.”
What Bolt will generate:
const express = require('express');
const mongoose = require('mongoose');
const argon2 = require('argon2');
const crypto = require('crypto');
const app = express();
app.use(express.json());
// User model with secure password storage
const UserSchema = new mongoose.Schema({
email: {
type: String,
required: true,
unique: true,
trim: true,
lowercase: true
},
passwordHash: {
type: String,
required: true
},
name: String,
createdAt: {
type: Date,
default: Date.now
}
});
// Never include password in JSON responses
UserSchema.set('toJSON', {
transform: function(doc, ret, options) {
delete ret.passwordHash;
return ret;
}
});
const User = mongoose.model('User', UserSchema);
// Registration endpoint with secure password hashing
app.post('/register', async (req, res) => {
try {
const { email, password, name } = req.body;
// Input validation
if (!email || !password) {
return res.status(400).json({ message: 'Email and password are required' });
}
if (password.length < 8) {
return res.status(400).json({ message: 'Password must be at least 8 characters' });
}
// Check if user already exists
const existingUser = await User.findOne({ email });
if (existingUser) {
return res.status(400).json({ message: 'User already exists' });
}
// Hash password with Argon2id (recommended variant)
const passwordHash = await argon2.hash(password, {
type: argon2.argon2id,
memoryCost: 65536, // 64 MiB
timeCost: 3, // 3 iterations
parallelism: 4, // 4 parallel threads
saltLength: 16 // 16 bytes salt
});
// Create new user with hashed password
const user = new User({
email,
passwordHash, // Only store the hash, never the original password
name
});
await user.save();
// Don't include password hash in response
res.status(201).json({
message: 'User created successfully',
user: {
id: user._id,
email: user.email,
name: user.name
}
});
} catch (error) {
console.error('Registration error:', error);
res.status(500).json({ message: 'Server error' });
}
});
// Login endpoint with secure password verification
app.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
// Input validation
if (!email || !password) {
return res.status(400).json({ message: 'Email and password are required' });
}
// Find user by email
const user = await User.findOne({ email });
// Use constant-time verification to prevent timing attacks
// Always verify against a hash, even if user doesn't exist
const validPassword = user
? await argon2.verify(user.passwordHash, password)
: await argon2.verify(await argon2.hash('dummy_password'), 'invalid');
if (!user || !validPassword) {
// Don't reveal whether the user exists or password is wrong
return res.status(401).json({ message: 'Invalid credentials' });
}
// Generate session or JWT token here
// ...
res.json({
message: 'Login successful',
user: {
id: user._id,
email: user.email,
name: user.name
}
// Include token here
});
} catch (error) {
console.error('Login error:', error);
res.status(500).json({ message: 'Server error' });
}
});
// Connect to MongoDB and start server
mongoose.connect('mongodb://localhost/secure_auth_db')
.then(() => {
app.listen(3000, () => {
console.log('Server running on port 3000');
});
})
.catch(err => console.error('Database connection error:', err));
This implementation addresses the vulnerabilities by:
- Using Argon2id, the winner of the Password Hashing Competition and current best practice
- Generating a unique cryptographic salt for each user
- Using appropriate memory and time cost parameters to make brute force attacks impractical
- Never storing the original password, only the hash
- Using constant-time verification to prevent timing attacks
- Providing generic error messages that don’t reveal whether a user exists
Prompt 2: Implementing Password Validation and Security Policies
Instead of asking: “Add password validation to my registration form”
Use this prompt: “Implement comprehensive password validation and security policies in my Bolt application. Create a password validation service that enforces minimum length, complexity requirements (uppercase, lowercase, numbers, special characters), and checks against common passwords and dictionary words. Implement password expiration, history tracking to prevent reuse of previous passwords, and account lockout after failed attempts. Ensure all password-related operations use secure hashing with bcrypt and proper error handling.”
What Bolt will generate:
const bcrypt = require('bcrypt');
const zxcvbn = require('zxcvbn'); // Password strength estimator
// Password policy configuration
const PASSWORD_POLICY = {
minLength: 10,
requireUppercase: true,
requireLowercase: true,
requireNumbers: true,
requireSpecial: true,
maxAge: 90 * 24 * 60 * 60 * 1000, // 90 days in milliseconds
historySize: 5, // Remember last 5 passwords
maxAttempts: 5, // Lock after 5 failed attempts
lockDuration: 15 * 60 * 1000 // 15 minutes in milliseconds
};
// Enhanced User Schema with password security features
const UserSchema = new mongoose.Schema({
email: { type: String, required: true, unique: true },
passwordHash: { type: String, required: true },
passwordHistory: [{ type: String }], // Store previous password hashes
passwordLastChanged: { type: Date, default: Date.now },
loginAttempts: { type: Number, default: 0 },
lockUntil: { type: Date },
name: String,
createdAt: { type: Date, default: Date.now }
});
// Password validation service
class PasswordService {
// Validate password strength and policy compliance
static validatePassword(password, email) {
const errors = [];
// Check minimum length
if (password.length < PASSWORD_POLICY.minLength) {
errors.push(`Password must be at least ${PASSWORD_POLICY.minLength} characters long`);
}
// Check character requirements
if (PASSWORD_POLICY.requireUppercase && !/[A-Z]/.test(password)) {
errors.push('Password must contain at least one uppercase letter');
}
if (PASSWORD_POLICY.requireLowercase && !/[a-z]/.test(password)) {
errors.push('Password must contain at least one lowercase letter');
}
if (PASSWORD_POLICY.requireNumbers && !/[0-9]/.test(password)) {
errors.push('Password must contain at least one number');
}
if (PASSWORD_POLICY.requireSpecial && !/[^A-Za-z0-9]/.test(password)) {
errors.push('Password must contain at least one special character');
}
// Check for common passwords and patterns using zxcvbn
const strength = zxcvbn(password, [email]); // Include email as user input to avoid related passwords
if (strength.score < 3) {
errors.push('Password is too weak or common. Please choose a stronger password.');
// Add specific feedback from zxcvbn if available
if (strength.feedback.warning) {
errors.push(strength.feedback.warning);
}
}
return {
valid: errors.length === 0,
errors: errors
};
}
// Hash password using bcrypt
static async hashPassword(password) {
const saltRounds = 12; // Higher is more secure but slower
return bcrypt.hash(password, saltRounds);
}
// Verify password against stored hash
static async verifyPassword(password, hash) {
return bcrypt.compare(password, hash);
}
// Check if password is in user's history
static async isPasswordInHistory(user, password) {
// Check against current password
if (await bcrypt.compare(password, user.passwordHash)) {
return true;
}
// Check against password history
for (const historicalHash of user.passwordHistory) {
if (await bcrypt.compare(password, historicalHash)) {
return true;
}
}
return false;
}
// Update password with history tracking
static async updatePassword(user, newPassword) {
// Hash the new password
const newHash = await this.hashPassword(newPassword);
// Add current password to history before updating
user.passwordHistory.push(user.passwordHash);
// Limit history size
if (user.passwordHistory.length > PASSWORD_POLICY.historySize) {
user.passwordHistory = user.passwordHistory.slice(-PASSWORD_POLICY.historySize);
}
// Update password and reset timestamp
user.passwordHash = newHash;
user.passwordLastChanged = new Date();
user.loginAttempts = 0;
user.lockUntil = undefined;
return user.save();
}
// Check if password is expired
static isPasswordExpired(user) {
if (!PASSWORD_POLICY.maxAge) return false;
const now = new Date();
const expiryDate = new Date(user.passwordLastChanged.getTime() + PASSWORD_POLICY.maxAge);
return now > expiryDate;
}
// Handle failed login attempt
static async handleFailedLogin(user) {
// Increment login attempts
user.loginAttempts += 1;
// Lock account if max attempts reached
if (user.loginAttempts >= PASSWORD_POLICY.maxAttempts) {
user.lockUntil = new Date(Date.now() + PASSWORD_POLICY.lockDuration);
}
await user.save();
return user;
}
// Check if account is locked
static isAccountLocked(user) {
return user.lockUntil && user.lockUntil > new Date();
}
}
// Registration endpoint with enhanced password validation
app.post('/register', async (req, res) => {
try {
const { email, password, name } = req.body;
// Validate password
const validation = PasswordService.validatePassword(password, email);
if (!validation.valid) {
return res.status(400).json({
message: 'Password does not meet security requirements',
errors: validation.errors
});
}
// Check if user already exists
const existingUser = await User.findOne({ email });
if (existingUser) {
return res.status(400).json({ message: 'User already exists' });
}
// Hash password
const passwordHash = await PasswordService.hashPassword(password);
// Create new user with secure password
const user = new User({
email,
passwordHash,
name,
passwordHistory: [], // Initialize empty history
passwordLastChanged: new Date()
});
await user.save();
res.status(201).json({
message: 'User created successfully',
user: {
id: user._id,
email: user.email,
name: user.name
}
});
} catch (error) {
console.error('Registration error:', error);
res.status(500).json({ message: 'Server error' });
}
});
// Login endpoint with account lockout and password expiration checks
app.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
// Find user by email
const user = await User.findOne({ email });
// Check if user exists
if (!user) {
// Use constant time comparison to prevent timing attacks
await bcrypt.compare(password, '$2b$12$' + 'a'.repeat(53));
return res.status(401).json({ message: 'Invalid credentials' });
}
// Check if account is locked
if (PasswordService.isAccountLocked(user)) {
const lockRemaining = Math.ceil((user.lockUntil - new Date()) / 1000 / 60);
return res.status(401).json({
message: `Account is locked. Try again in ${lockRemaining} minutes.`
});
}
// Verify password
const validPassword = await PasswordService.verifyPassword(password, user.passwordHash);
if (!validPassword) {
await PasswordService.handleFailedLogin(user);
return res.status(401).json({ message: 'Invalid credentials' });
}
// Reset login attempts on successful login
user.loginAttempts = 0;
await user.save();
// Check if password is expired
if (PasswordService.isPasswordExpired(user)) {
return res.status(403).json({
message: 'Password expired. Please reset your password.',
passwordExpired: true
});
}
// Generate session or JWT token here
// ...
res.json({
message: 'Login successful',
user: {
id: user._id,
email: user.email,
name: user.name
}
// Include token here
});
} catch (error) {
console.error('Login error:', error);
res.status(500).json({ message: 'Server error' });
}
});
// Password change endpoint with history check
app.post('/change-password', async (req, res) => {
try {
const { userId, currentPassword, newPassword } = req.body;
// Find user
const user = await User.findById(userId);
if (!user) {
return res.status(404).json({ message: 'User not found' });
}
// Verify current password
const validPassword = await PasswordService.verifyPassword(currentPassword, user.passwordHash);
if (!validPassword) {
return res.status(401).json({ message: 'Current password is incorrect' });
}
// Validate new password
const validation = PasswordService.validatePassword(newPassword, user.email);
if (!validation.valid) {
return res.status(400).json({
message: 'New password does not meet security requirements',
errors: validation.errors
});
}
// Check if new password is in history
const inHistory = await PasswordService.isPasswordInHistory(user, newPassword);
if (inHistory) {
return res.status(400).json({
message: `Cannot reuse any of your last ${PASSWORD_POLICY.historySize} passwords`
});
}
// Update password
await PasswordService.updatePassword(user, newPassword);
res.json({ message: 'Password changed successfully' });
} catch (error) {
console.error('Password change error:', error);
res.status(500).json({ message: 'Server error' });
}
});
This implementation addresses password security comprehensively by:
- Enforcing strong password policies with zxcvbn for realistic strength estimation
- Implementing password history to prevent reuse
- Adding password expiration to ensure regular updates
- Including account lockout to prevent brute force attacks
- Using bcrypt with appropriate work factor for secure hashing
- Providing detailed feedback on password requirements
Prompt 3: Implementing Secure Password Reset Functionality
Instead of asking: “Add a forgot password feature to my app”
Use this prompt: “Implement a secure password reset system in my Bolt application using cryptographically secure tokens with limited validity. Create endpoints for requesting a reset, validating the token, and setting a new password. Generate single-use tokens with proper entropy, store only hashed tokens in the database with expiration timestamps, and invalidate tokens after use. Ensure the implementation is resistant to timing attacks, token enumeration, and includes rate limiting to prevent abuse. Use scrypt for token hashing and include proper logging of reset attempts.”
What Bolt will generate:
const crypto = require('crypto');
const scrypt = require('scrypt-kdf');
const nodemailer = require('nodemailer');
const { promisify } = require('util');
// Enhanced User Schema with password reset fields
const UserSchema = new mongoose.Schema({
email: { type: String, required: true, unique: true },
passwordHash: { type: String, required: true },
resetToken: { type: String }, // Stores only the hashed token
resetTokenExpires: { type: Date },
resetAttempts: { type: Number, default: 0 },
lastResetRequest: { type: Date }
});
// Password reset service
class PasswordResetService {
// Generate a secure random token
static async generateToken() {
// 32 bytes = 256 bits of entropy
const buffer = await promisify(crypto.randomBytes)(32);
return buffer.toString('hex');
}
// Hash a token using scrypt (more secure than bcrypt for this use case)
static async hashToken(token) {
const result = await scrypt.kdf(token, {
logN: 15, // CPU/memory cost parameter
r: 8, // Block size
p: 1 // Parallelization
});
return result.toString('base64');
}
// Verify a token against a hash
static async verifyToken(token, hash) {
try {
const hashBuffer = Buffer.from(hash, 'base64');
return await scrypt.verify(Buffer.from(token), hashBuffer);
} catch (error) {
return false;
}
}
// Create a reset token for a user
static async createResetToken(user) {
// Rate limiting: only allow 3 reset requests per 24 hours
const now = new Date();
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
if (user.lastResetRequest && user.lastResetRequest > oneDayAgo) {
user.resetAttempts = (user.resetAttempts || 0) + 1;
if (user.resetAttempts >= 3) {
throw new Error('Too many password reset requests. Please try again tomorrow.');
}
} else {
user.resetAttempts = 1;
}
user.lastResetRequest = now;
// Generate token
const token = await this.generateToken();
// Hash token before storing
const hashedToken = await this.hashToken(token);
// Set expiration (1 hour)
const expiration = new Date();
expiration.setHours(expiration.getHours() + 1);
// Update user
user.resetToken = hashedToken;
user.resetTokenExpires = expiration;
await user.save();
// Return plain token to be sent via email
return token;
}
// Validate a reset token
static async validateToken(email, token) {
// Find user by email
const user = await User.findOne({
email,
resetTokenExpires: { $gt: new Date() }
});
if (!user || !user.resetToken) {
// Use constant time comparison to prevent timing attacks
// This simulates the verification process even if user doesn't exist
await this.verifyToken(token, 'U2NyeXB0AAEAAAAIAAAAASUXHnR8BZwy1G+XBZEXlxEKAAA=');
return { valid: false, user: null };
}
// Verify token
const isValid = await this.verifyToken(token, user.resetToken);
return {
valid: isValid,
user: isValid ? user : null
};
}
// Reset password and invalidate token
static async resetPassword(user, newPassword) {
// Hash new password
const saltRounds = 12;
const passwordHash = await bcrypt.hash(newPassword, saltRounds);
// Update user
user.passwordHash = passwordHash;
user.resetToken = null;
user.resetTokenExpires = null;
await user.save();
return user;
}
// Send password reset email
static async sendResetEmail(email, token, req) {
// Create reset URL
const resetUrl = `${req.protocol}://${req.get('host')}/reset-password/${token}`;
// Configure email transport (replace with your SMTP settings)
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: process.env.SMTP_PORT,
secure: process.env.SMTP_SECURE === 'true',
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS
}
});
// Email content
const mailOptions = {
from: process.env.EMAIL_FROM,
to: email,
subject: 'Password Reset Request',
text: `You requested a password reset. Please use the following link to reset your password. This link is valid for 1 hour.\n\n${resetUrl}\n\nIf you did not request this, please ignore this email and your password will remain unchanged.`,
html: `
<p>You requested a password reset.</p>
<p>Please use the following link to reset your password. This link is valid for 1 hour.</p>
<p><a href="${resetUrl}">Reset Password</a></p>
<p>If you did not request this, please ignore this email and your password will remain unchanged.</p>
`
};
// Send email
await transporter.sendMail(mailOptions);
}
}
// Request password reset endpoint
app.post('/forgot-password', async (req, res) => {
try {
const { email } = req.body;
// Always return the same response regardless of whether the email exists
// This prevents email enumeration attacks
// Find user by email
const user = await User.findOne({ email });
if (user) {
try {
// Create reset token
const token = await PasswordResetService.createResetToken(user);
// Send reset email
await PasswordResetService.sendResetEmail(email, token, req);
// Log the reset request (but not the token)
console.log(`Password reset requested for ${email} at ${new Date()}`);
} catch (error) {
// Log error but don't expose to user
console.error(`Error creating reset token: ${error.message}`);
}
} else {
// Simulate work to prevent timing attacks
await bcrypt.hash('dummy_password', 10);
// Log the attempt for non-existent user
console.log(`Password reset attempted for non-existent email: ${email} at ${new Date()}`);
}
// Same response whether user exists or not
res.json({
message: 'If a user with that email exists, a password reset link has been sent.'
});
} catch (error) {
console.error('Password reset request error:', error);
res.status(500).json({ message: 'Server error' });
}
});
// Validate reset token endpoint
app.get('/reset-password/:token', async (req, res) => {
try {
const { token } = req.params;
const { email } = req.query;
if (!email || !token) {
return res.status(400).json({ message: 'Email and token are required' });
}
// Validate token
const { valid } = await PasswordResetService.validateToken(email, token);
if (!valid) {
return res.status(400).json({ message: 'Invalid or expired password reset token' });
}
res.json({ message: 'Token is valid', valid: true });
} catch (error) {
console.error('Token validation error:', error);
res.status(500).json({ message: 'Server error' });
}
});
// Reset password endpoint
app.post('/reset-password', async (req, res) => {
try {
const { email, token, newPassword } = req.body;
if (!email || !token || !newPassword) {
return res.status(400).json({
message: 'Email, token, and new password are required'
});
}
// Validate password
const validation = PasswordService.validatePassword(newPassword, email);
if (!validation.valid) {
return res.status(400).json({
message: 'Password does not meet security requirements',
errors: validation.errors
});
}
// Validate token
const { valid, user } = await PasswordResetService.validateToken(email, token);
if (!valid || !user) {
return res.status(400).json({ message: 'Invalid or expired password reset token' });
}
// Reset password
await PasswordResetService.resetPassword(user, newPassword);
// Log successful reset
console.log(`Password reset completed for ${email} at ${new Date()}`);
res.json({ message: 'Password has been reset successfully' });
} catch (error) {
console.error('Password reset error:', error);
res.status(500).json({ message: 'Server error' });
}
});
This implementation provides a secure password reset system by:
- Using cryptographically secure random tokens with high entropy
- Storing only hashed tokens in the database
- Setting short expiration times for tokens (1 hour)
- Implementing rate limiting to prevent abuse
- Using constant-time comparisons to prevent timing attacks
- Providing identical responses whether a user exists or not to prevent enumeration
- Using scrypt for token hashing, which is memory-hard and resistant to hardware acceleration
- Including proper logging for security auditing
Best Practices for Password Storage
-
Hashing Algorithms:
- Use modern algorithms like Argon2id or bcrypt
- Never use MD5, SHA-1, or other cryptographic hash functions
- Configure proper work factors based on your server capabilities
- Regularly review and update algorithm parameters
-
Salt Management:
- Generate unique cryptographic salt for each password
- Use secure random number generators
- Store salt alongside the hash
- Never reuse salts across passwords
-
Password Validation:
- Implement strong password policies
- Use zxcvbn for realistic password strength estimation
- Provide clear feedback on password requirements
- Avoid common password patterns
-
Security Features:
- Implement account lockout after failed attempts
- Add password expiration policies
- Track password history to prevent reuse
- Use secure password reset mechanisms
Conclusion
Implementing secure password storage in Bolt applications requires careful attention to multiple security aspects. By following the best practices outlined in this guide and implementing proper security measures, you can create a robust password storage system that effectively protects your users’ credentials.
Remember that security is an ongoing process. Regularly review and update your password storage implementation to address new threats and vulnerabilities. Stay informed about the latest security recommendations and adjust your implementation accordingly.