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 { createCookieSessionStorage, redirect } from '@remix-run/cloudflare';
|
||||||
import { createScopedLogger } from '~/utils/logger';
|
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 encoder = new TextEncoder();
|
||||||
const decoder = new TextDecoder();
|
const decoder = new TextDecoder();
|
||||||
const IV_LENGTH = 16;
|
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 type { LoaderFunctionArgs, ActionFunctionArgs } from '@remix-run/cloudflare';
|
||||||
import { createScopedLogger } from '~/utils/logger';
|
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 { useState } from 'react';
|
||||||
import { json, redirect } from '@remix-run/cloudflare';
|
import { json, redirect } from '@remix-run/cloudflare';
|
||||||
import { Form, useActionData, useNavigation } from '@remix-run/react';
|
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 { encrypt, decrypt } from '~/lib/crypto';
|
||||||
import { createScopedLogger } from './logger';
|
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