mirror of
https://github.com/stackblitz/bolt.new
synced 2025-06-26 18:17:50 +00:00
Add comprehensive security enhancements
- Enhanced encryption system with AES-GCM and PBKDF2 - Added secure authentication system with session management - Implemented security middleware with headers and rate limiting - Created secure storage utilities for sensitive data - Added login system with brute force protection - Implemented security components (password strength, 2FA) - Added security utility functions for threat detection Copyright (c) 2024 Ervin Remus Radosavlevici
This commit is contained in:
parent
3c5db34c66
commit
2975bc3d45
103
app/components/security/PasswordStrengthMeter.tsx
Normal file
103
app/components/security/PasswordStrengthMeter.tsx
Normal file
@ -0,0 +1,103 @@
|
||||
/**
|
||||
* Password strength meter component
|
||||
* Copyright (c) 2024 Ervin Remus Radosavlevici
|
||||
* All rights reserved.
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
interface PasswordStrengthMeterProps {
|
||||
password: string;
|
||||
}
|
||||
|
||||
export default function PasswordStrengthMeter({ password }: PasswordStrengthMeterProps) {
|
||||
const [strength, setStrength] = useState(0);
|
||||
const [feedback, setFeedback] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
// Calculate password strength
|
||||
const calculateStrength = (pwd: string) => {
|
||||
if (!pwd) {
|
||||
setStrength(0);
|
||||
setFeedback('');
|
||||
return;
|
||||
}
|
||||
|
||||
let score = 0;
|
||||
|
||||
// Length check
|
||||
if (pwd.length >= 8) score += 1;
|
||||
if (pwd.length >= 12) score += 1;
|
||||
|
||||
// Complexity checks
|
||||
if (/[A-Z]/.test(pwd)) score += 1; // Has uppercase
|
||||
if (/[a-z]/.test(pwd)) score += 1; // Has lowercase
|
||||
if (/[0-9]/.test(pwd)) score += 1; // Has number
|
||||
if (/[^A-Za-z0-9]/.test(pwd)) score += 1; // Has special char
|
||||
|
||||
// Variety check
|
||||
const uniqueChars = new Set(pwd.split('')).size;
|
||||
if (uniqueChars > pwd.length * 0.7) score += 1;
|
||||
|
||||
// Common patterns check (reduce score)
|
||||
if (/^123|password|admin|qwerty|welcome/i.test(pwd)) score -= 2;
|
||||
|
||||
// Normalize score to 0-4 range
|
||||
const normalizedScore = Math.max(0, Math.min(4, score));
|
||||
|
||||
setStrength(normalizedScore);
|
||||
|
||||
// Set feedback based on score
|
||||
switch (normalizedScore) {
|
||||
case 0:
|
||||
setFeedback('Very weak');
|
||||
break;
|
||||
case 1:
|
||||
setFeedback('Weak - Add more characters and variety');
|
||||
break;
|
||||
case 2:
|
||||
setFeedback('Fair - Consider adding special characters');
|
||||
break;
|
||||
case 3:
|
||||
setFeedback('Good - Password has decent strength');
|
||||
break;
|
||||
case 4:
|
||||
setFeedback('Strong - Excellent password strength');
|
||||
break;
|
||||
default:
|
||||
setFeedback('');
|
||||
}
|
||||
};
|
||||
|
||||
calculateStrength(password);
|
||||
}, [password]);
|
||||
|
||||
// Determine color based on strength
|
||||
const getColor = () => {
|
||||
switch (strength) {
|
||||
case 0: return 'bg-gray-300';
|
||||
case 1: return 'bg-red-500';
|
||||
case 2: return 'bg-orange-500';
|
||||
case 3: return 'bg-yellow-500';
|
||||
case 4: return 'bg-green-500';
|
||||
default: return 'bg-gray-300';
|
||||
}
|
||||
};
|
||||
|
||||
// Skip rendering if no password
|
||||
if (!password) return null;
|
||||
|
||||
return (
|
||||
<div className="mt-2">
|
||||
<div className="flex gap-1 h-1 mb-1">
|
||||
{[...Array(4)].map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`h-full flex-1 rounded-sm ${i < strength ? getColor() : 'bg-gray-200'}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-xs text-gray-600">{feedback}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
141
app/components/security/TwoFactorAuth.tsx
Normal file
141
app/components/security/TwoFactorAuth.tsx
Normal file
@ -0,0 +1,141 @@
|
||||
/**
|
||||
* Two-factor authentication component
|
||||
* Copyright (c) 2024 Ervin Remus Radosavlevici
|
||||
* All rights reserved.
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
interface TwoFactorAuthProps {
|
||||
onVerify: (code: string) => Promise<boolean>;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export default function TwoFactorAuth({ onVerify, onCancel }: TwoFactorAuthProps) {
|
||||
const [code, setCode] = useState(['', '', '', '', '', '']);
|
||||
const [error, setError] = useState('');
|
||||
const [isVerifying, setIsVerifying] = useState(false);
|
||||
|
||||
// Handle input change for each digit
|
||||
const handleChange = (index: number, value: string) => {
|
||||
// Only allow numbers
|
||||
if (value && !/^\d+$/.test(value)) return;
|
||||
|
||||
const newCode = [...code];
|
||||
newCode[index] = value;
|
||||
setCode(newCode);
|
||||
|
||||
// Auto-focus next input
|
||||
if (value && index < 5) {
|
||||
const nextInput = document.getElementById(`2fa-input-${index + 1}`);
|
||||
nextInput?.focus();
|
||||
}
|
||||
|
||||
// Clear error when typing
|
||||
if (error) setError('');
|
||||
};
|
||||
|
||||
// Handle key down for backspace navigation
|
||||
const handleKeyDown = (index: number, e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Backspace' && !code[index] && index > 0) {
|
||||
const prevInput = document.getElementById(`2fa-input-${index - 1}`);
|
||||
prevInput?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
// Handle paste event to fill all inputs
|
||||
const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
|
||||
e.preventDefault();
|
||||
const pastedData = e.clipboardData.getData('text/plain').trim();
|
||||
|
||||
// Check if pasted content is a valid 6-digit code
|
||||
if (/^\d{6}$/.test(pastedData)) {
|
||||
const digits = pastedData.split('');
|
||||
setCode(digits);
|
||||
|
||||
// Focus the last input
|
||||
const lastInput = document.getElementById('2fa-input-5');
|
||||
lastInput?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
// Handle verification
|
||||
const handleVerify = async () => {
|
||||
const fullCode = code.join('');
|
||||
|
||||
// Validate code format
|
||||
if (fullCode.length !== 6 || !/^\d{6}$/.test(fullCode)) {
|
||||
setError('Please enter a valid 6-digit code');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsVerifying(true);
|
||||
setError('');
|
||||
|
||||
try {
|
||||
const isValid = await onVerify(fullCode);
|
||||
|
||||
if (!isValid) {
|
||||
setError('Invalid verification code. Please try again.');
|
||||
setCode(['', '', '', '', '', '']);
|
||||
// Focus first input
|
||||
document.getElementById('2fa-input-0')?.focus();
|
||||
}
|
||||
} catch (err) {
|
||||
setError('An error occurred during verification. Please try again.');
|
||||
} finally {
|
||||
setIsVerifying(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-white dark:bg-gray-800 rounded-lg shadow-md">
|
||||
<h2 className="text-xl font-semibold mb-4">Two-Factor Authentication</h2>
|
||||
<p className="mb-6 text-gray-600 dark:text-gray-300">
|
||||
Enter the 6-digit code from your authenticator app
|
||||
</p>
|
||||
|
||||
<div className="flex justify-center gap-2 mb-6">
|
||||
{code.map((digit, index) => (
|
||||
<input
|
||||
key={index}
|
||||
id={`2fa-input-${index}`}
|
||||
type="text"
|
||||
maxLength={1}
|
||||
value={digit}
|
||||
onChange={(e) => handleChange(index, e.target.value)}
|
||||
onKeyDown={(e) => handleKeyDown(index, e)}
|
||||
onPaste={index === 0 ? handlePaste : undefined}
|
||||
className="w-12 h-12 text-center text-xl border rounded-md focus:border-blue-500 focus:ring-1 focus:ring-blue-500"
|
||||
autoFocus={index === 0}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="text-red-500 text-center mb-4" role="alert">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-between">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
className="px-4 py-2 text-gray-700 border border-gray-300 rounded-md hover:bg-gray-100"
|
||||
disabled={isVerifying}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleVerify}
|
||||
disabled={code.join('').length !== 6 || isVerifying}
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 disabled:opacity-50"
|
||||
>
|
||||
{isVerifying ? 'Verifying...' : 'Verify'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,3 +1,9 @@
|
||||
/**
|
||||
* Authentication system
|
||||
* Copyright (c) 2024 Ervin Remus Radosavlevici
|
||||
* All rights reserved.
|
||||
*/
|
||||
|
||||
import { createCookieSessionStorage, redirect } from '@remix-run/cloudflare';
|
||||
import { createScopedLogger } from '~/utils/logger';
|
||||
|
||||
|
@ -1,3 +1,9 @@
|
||||
/**
|
||||
* Enhanced encryption utilities
|
||||
* Copyright (c) 2024 Ervin Remus Radosavlevici
|
||||
* All rights reserved.
|
||||
*/
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const decoder = new TextDecoder();
|
||||
const IV_LENGTH = 16;
|
||||
|
@ -1,3 +1,9 @@
|
||||
/**
|
||||
* Security middleware
|
||||
* Copyright (c) 2024 Ervin Remus Radosavlevici
|
||||
* All rights reserved.
|
||||
*/
|
||||
|
||||
import type { LoaderFunctionArgs, ActionFunctionArgs } from '@remix-run/cloudflare';
|
||||
import { createScopedLogger } from '~/utils/logger';
|
||||
|
||||
|
@ -1,3 +1,9 @@
|
||||
/**
|
||||
* Secure login route
|
||||
* Copyright (c) 2024 Ervin Remus Radosavlevici
|
||||
* All rights reserved.
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
import { json, redirect } from '@remix-run/cloudflare';
|
||||
import { Form, useActionData, useNavigation } from '@remix-run/react';
|
||||
|
@ -1,3 +1,9 @@
|
||||
/**
|
||||
* Secure storage utilities
|
||||
* Copyright (c) 2024 Ervin Remus Radosavlevici
|
||||
* All rights reserved.
|
||||
*/
|
||||
|
||||
import { encrypt, decrypt } from '~/lib/crypto';
|
||||
import { createScopedLogger } from './logger';
|
||||
|
||||
|
160
app/utils/securityUtils.ts
Normal file
160
app/utils/securityUtils.ts
Normal file
@ -0,0 +1,160 @@
|
||||
/**
|
||||
* Security utility functions
|
||||
* Copyright (c) 2024 Ervin Remus Radosavlevici
|
||||
* All rights reserved.
|
||||
*/
|
||||
|
||||
import { createScopedLogger } from './logger';
|
||||
|
||||
const logger = createScopedLogger('SecurityUtils');
|
||||
|
||||
/**
|
||||
* Sanitize user input to prevent XSS attacks
|
||||
* @param input - User input string to sanitize
|
||||
* @returns Sanitized string
|
||||
*/
|
||||
export function sanitizeInput(input: string): string {
|
||||
if (!input) return '';
|
||||
|
||||
return input
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate email format
|
||||
* @param email - Email to validate
|
||||
* @returns True if email format is valid
|
||||
*/
|
||||
export function isValidEmail(email: string): boolean {
|
||||
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
||||
return emailRegex.test(email);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a secure random token
|
||||
* @param length - Length of the token
|
||||
* @returns Random token string
|
||||
*/
|
||||
export function generateSecureToken(length = 32): string {
|
||||
const array = new Uint8Array(length);
|
||||
crypto.getRandomValues(array);
|
||||
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a password meets security requirements
|
||||
* @param password - Password to check
|
||||
* @returns Object with validation result and feedback
|
||||
*/
|
||||
export function validatePassword(password: string): {
|
||||
isValid: boolean;
|
||||
feedback: string;
|
||||
} {
|
||||
if (!password) {
|
||||
return { isValid: false, feedback: 'Password is required' };
|
||||
}
|
||||
|
||||
if (password.length < 8) {
|
||||
return { isValid: false, feedback: 'Password must be at least 8 characters long' };
|
||||
}
|
||||
|
||||
// Check for complexity requirements
|
||||
const hasUppercase = /[A-Z]/.test(password);
|
||||
const hasLowercase = /[a-z]/.test(password);
|
||||
const hasNumbers = /[0-9]/.test(password);
|
||||
const hasSpecialChars = /[^A-Za-z0-9]/.test(password);
|
||||
|
||||
const requirementsMet = [hasUppercase, hasLowercase, hasNumbers, hasSpecialChars]
|
||||
.filter(Boolean).length;
|
||||
|
||||
if (requirementsMet < 3) {
|
||||
return {
|
||||
isValid: false,
|
||||
feedback: 'Password must contain at least 3 of the following: uppercase letters, lowercase letters, numbers, and special characters'
|
||||
};
|
||||
}
|
||||
|
||||
// Check for common passwords
|
||||
const commonPasswords = [
|
||||
'password', 'admin', '123456', 'qwerty', 'welcome',
|
||||
'letmein', 'monkey', 'password123', 'abc123'
|
||||
];
|
||||
|
||||
if (commonPasswords.some(common =>
|
||||
password.toLowerCase().includes(common.toLowerCase()))) {
|
||||
return {
|
||||
isValid: false,
|
||||
feedback: 'Password contains common words that are easily guessed'
|
||||
};
|
||||
}
|
||||
|
||||
return { isValid: true, feedback: 'Password meets security requirements' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect potential security threats in user input
|
||||
* @param input - User input to analyze
|
||||
* @returns True if potential threat detected
|
||||
*/
|
||||
export function detectSecurityThreats(input: string): boolean {
|
||||
if (!input) return false;
|
||||
|
||||
// Check for common SQL injection patterns
|
||||
const sqlInjectionPatterns = [
|
||||
/'\s*OR\s*'1'\s*=\s*'1/i,
|
||||
/'\s*OR\s*1\s*=\s*1/i,
|
||||
/'\s*;\s*DROP\s+TABLE/i,
|
||||
/'\s*;\s*DELETE\s+FROM/i,
|
||||
/UNION\s+SELECT/i
|
||||
];
|
||||
|
||||
// Check for common XSS patterns
|
||||
const xssPatterns = [
|
||||
/<script>/i,
|
||||
/javascript:/i,
|
||||
/onerror=/i,
|
||||
/onload=/i,
|
||||
/onclick=/i
|
||||
];
|
||||
|
||||
// Check for path traversal attempts
|
||||
const pathTraversalPatterns = [
|
||||
/\.\.\//,
|
||||
/\.\.\\\\/
|
||||
];
|
||||
|
||||
const allPatterns = [
|
||||
...sqlInjectionPatterns,
|
||||
...xssPatterns,
|
||||
...pathTraversalPatterns
|
||||
];
|
||||
|
||||
const threatDetected = allPatterns.some(pattern => pattern.test(input));
|
||||
|
||||
if (threatDetected) {
|
||||
logger.warn('Security threat detected in user input');
|
||||
}
|
||||
|
||||
return threatDetected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Content Security Policy header value
|
||||
* @returns CSP header value
|
||||
*/
|
||||
export function generateCSP(): string {
|
||||
return [
|
||||
"default-src 'self'",
|
||||
"script-src 'self' 'unsafe-inline' 'unsafe-eval'",
|
||||
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
|
||||
"img-src 'self' data: blob:",
|
||||
"font-src 'self' https://fonts.gstatic.com",
|
||||
"connect-src 'self' https://*.anthropic.com",
|
||||
"frame-src 'self'",
|
||||
"object-src 'none'",
|
||||
"base-uri 'self'"
|
||||
].join('; ');
|
||||
}
|
Loading…
Reference in New Issue
Block a user