mirror of
https://github.com/R0m1k3/LogiFlow.git
synced 2026-10-11 17:27:31 +02:00
feat(security): ameliorations securite - CSRF, sanitization validator.js, eval removal
This commit is contained in:
1 parent
78adc2c5ae
commit
0f8920f326
5 files changed
+342
-35
No files matched your search
@@ -1,6 +1,14 @@
|
||||
import express, { type Request, Response, NextFunction } from "express";
|
||||
import cookieParser from "cookie-parser";
|
||||
import { registerRoutes } from "./routes.js";
|
||||
import { setupVite, serveStatic } from "./vite.js";
|
||||
import {
|
||||
setupSecurityHeaders,
|
||||
setupRateLimiting,
|
||||
setupInputSanitization,
|
||||
setupCsrfProtection,
|
||||
setupCsrfTokenEndpoint
|
||||
} from "./security.js";
|
||||
|
||||
// Forcer la création de la table webhook_bap_config au démarrage de l'application
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
@@ -16,9 +24,28 @@ console.log('✅ [STARTUP] Weather system initialized');
|
||||
|
||||
const app = express();
|
||||
|
||||
// Parse cookies (required for CSRF)
|
||||
app.use(cookieParser());
|
||||
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
app.use(express.urlencoded({ extended: false, limit: '10mb' }));
|
||||
|
||||
// Setup security middlewares
|
||||
console.log('🔐 [STARTUP] Setting up security middlewares...');
|
||||
setupSecurityHeaders(app);
|
||||
setupRateLimiting(app);
|
||||
setupInputSanitization(app);
|
||||
|
||||
// CSRF Protection (only in production to avoid dev friction)
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
setupCsrfProtection(app);
|
||||
console.log('✅ [STARTUP] CSRF protection enabled');
|
||||
}
|
||||
|
||||
// CSRF token endpoint (always available for frontend to fetch token)
|
||||
setupCsrfTokenEndpoint(app);
|
||||
console.log('✅ [STARTUP] Security middlewares configured');
|
||||
|
||||
app.use((req, res, next) => {
|
||||
const start = Date.now();
|
||||
res.on("finish", () => {
|
||||
|
||||
+2
-2
@@ -886,8 +886,8 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
size: parts.file.buffer.length
|
||||
});
|
||||
|
||||
// Importer form-data dynamiquement avec eval pour ESM
|
||||
const FormDataModule = await eval('import("form-data")');
|
||||
// Import dynamique standard pour ESM (sans eval)
|
||||
const FormDataModule = await import('form-data');
|
||||
const FormData = FormDataModule.default;
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
+248
-33
@@ -1,40 +1,139 @@
|
||||
import { Express, Request, Response, NextFunction } from 'express';
|
||||
import rateLimit from 'express-rate-limit';
|
||||
import validator from 'validator';
|
||||
import { randomBytes } from 'crypto';
|
||||
|
||||
// ============================================================================
|
||||
// CSRF Protection (Double Submit Cookie Pattern)
|
||||
// ============================================================================
|
||||
|
||||
const CSRF_COOKIE_NAME = 'csrf_token';
|
||||
const CSRF_HEADER_NAME = 'x-csrf-token';
|
||||
|
||||
/**
|
||||
* Generate a cryptographically secure CSRF token
|
||||
*/
|
||||
export function generateCsrfToken(): string {
|
||||
return randomBytes(32).toString('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup CSRF protection middleware using Double Submit Cookie pattern
|
||||
* This pattern works well with SPAs and doesn't require server-side session storage
|
||||
*/
|
||||
export function setupCsrfProtection(app: Express) {
|
||||
// Middleware to set CSRF cookie on every response
|
||||
app.use((req: Request, res: Response, next: NextFunction) => {
|
||||
// Only set cookie if not already present
|
||||
if (!req.cookies?.[CSRF_COOKIE_NAME]) {
|
||||
const token = generateCsrfToken();
|
||||
res.cookie(CSRF_COOKIE_NAME, token, {
|
||||
httpOnly: false, // Must be readable by JS for double submit
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'strict',
|
||||
maxAge: 24 * 60 * 60 * 1000 // 24 hours
|
||||
});
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
// Middleware to validate CSRF token on state-changing requests
|
||||
app.use((req: Request, res: Response, next: NextFunction) => {
|
||||
const safeMethods = ['GET', 'HEAD', 'OPTIONS'];
|
||||
|
||||
// Skip CSRF check for safe methods
|
||||
if (safeMethods.includes(req.method)) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// Skip CSRF check for API endpoints that use other auth (e.g., webhook callbacks)
|
||||
const csrfExemptPaths = [
|
||||
'/api/health',
|
||||
'/api/webhook', // External webhook callbacks
|
||||
];
|
||||
|
||||
if (csrfExemptPaths.some(path => req.path.startsWith(path))) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const cookieToken = req.cookies?.[CSRF_COOKIE_NAME];
|
||||
const headerToken = req.headers[CSRF_HEADER_NAME] as string;
|
||||
|
||||
// Validate CSRF token
|
||||
if (!cookieToken || !headerToken || cookieToken !== headerToken) {
|
||||
console.warn(`🚨 CSRF validation failed for ${req.method} ${req.path} from IP: ${req.ip}`);
|
||||
return res.status(403).json({
|
||||
error: 'CSRF token validation failed',
|
||||
message: 'Request rejected due to security validation failure'
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current CSRF token endpoint for frontend
|
||||
*/
|
||||
export function setupCsrfTokenEndpoint(app: Express) {
|
||||
app.get('/api/csrf-token', (req: Request, res: Response) => {
|
||||
let token = req.cookies?.[CSRF_COOKIE_NAME];
|
||||
|
||||
if (!token) {
|
||||
token = generateCsrfToken();
|
||||
res.cookie(CSRF_COOKIE_NAME, token, {
|
||||
httpOnly: false,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'strict',
|
||||
maxAge: 24 * 60 * 60 * 1000
|
||||
});
|
||||
}
|
||||
|
||||
res.json({ csrfToken: token });
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Security Headers
|
||||
// ============================================================================
|
||||
|
||||
// Headers de sécurité
|
||||
export function setupSecurityHeaders(app: Express) {
|
||||
app.use((req: Request, res: Response, next: NextFunction) => {
|
||||
// Protection contre les attaques XSS
|
||||
res.setHeader('X-Content-Type-Options', 'nosniff');
|
||||
res.setHeader('X-Frame-Options', 'DENY');
|
||||
res.setHeader('X-XSS-Protection', '1; mode=block');
|
||||
|
||||
|
||||
// Protection HTTPS
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
|
||||
}
|
||||
|
||||
// Politique de sécurité du contenu
|
||||
res.setHeader('Content-Security-Policy',
|
||||
|
||||
// Politique de sécurité du contenu (renforcée)
|
||||
res.setHeader('Content-Security-Policy',
|
||||
"default-src 'self'; " +
|
||||
"script-src 'self' 'unsafe-inline' 'unsafe-eval'; " +
|
||||
"style-src 'self' 'unsafe-inline'; " +
|
||||
"img-src 'self' data: https:; " +
|
||||
"connect-src 'self' ws: wss:; " +
|
||||
"font-src 'self' data:;"
|
||||
"font-src 'self' data:; " +
|
||||
"form-action 'self';" // Prevent form submissions to external sites
|
||||
);
|
||||
|
||||
|
||||
// Protection contre les attaques de référence
|
||||
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
|
||||
|
||||
|
||||
// Protection des données sensibles
|
||||
res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
|
||||
|
||||
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
// Limitation du taux de requêtes
|
||||
// ============================================================================
|
||||
// Rate Limiting
|
||||
// ============================================================================
|
||||
|
||||
export function setupRateLimiting(app: Express) {
|
||||
// Limiteur général
|
||||
const generalLimiter = rateLimit({
|
||||
@@ -45,14 +144,12 @@ export function setupRateLimiting(app: Express) {
|
||||
},
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
trustProxy: 1, // Configuration sécurisée pour Docker (1 proxy de confiance)
|
||||
skip: (req) => {
|
||||
// Skip rate limiting for health checks
|
||||
return req.path === '/api/health';
|
||||
}
|
||||
});
|
||||
|
||||
// Limiteur pour l'authentification
|
||||
// Limiteur pour l'authentification (strict)
|
||||
const authLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 5, // limite les tentatives de connexion
|
||||
@@ -61,20 +158,17 @@ export function setupRateLimiting(app: Express) {
|
||||
},
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
trustProxy: 1, // Configuration sécurisée pour Docker (1 proxy de confiance)
|
||||
});
|
||||
|
||||
// Limiteur pour l'API - adapté pour une utilisation normale
|
||||
// Limiteur pour l'API
|
||||
const apiLimiter = rateLimit({
|
||||
windowMs: 1 * 60 * 1000, // 1 minute
|
||||
max: process.env.NODE_ENV === 'development' ? 500 : 300, // 500 en dev, 300 en prod
|
||||
max: process.env.NODE_ENV === 'development' ? 500 : 300,
|
||||
message: {
|
||||
error: 'Limite API atteinte, veuillez ralentir vos requêtes.',
|
||||
},
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
trustProxy: 1, // Configuration sécurisée pour Docker (1 proxy de confiance)
|
||||
// Exclure certaines routes critiques du rate limiting strict
|
||||
skip: (req) => {
|
||||
return req.path === '/api/health' || req.path === '/api/user';
|
||||
},
|
||||
@@ -91,52 +185,173 @@ export function setupRateLimiting(app: Express) {
|
||||
app.use('/api/', apiLimiter);
|
||||
}
|
||||
|
||||
// Validation et nettoyage des entrées
|
||||
// ============================================================================
|
||||
// Input Sanitization (Using validator.js)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Sanitize a single string value using validator.js
|
||||
* Protects against XSS, SQL injection patterns, and path traversal
|
||||
*/
|
||||
export function sanitizeString(input: string): string {
|
||||
if (typeof input !== 'string') return input;
|
||||
|
||||
let sanitized = input.trim();
|
||||
|
||||
// Escape HTML entities to prevent XSS
|
||||
sanitized = validator.escape(sanitized);
|
||||
|
||||
// Remove null bytes (used in some injection attacks)
|
||||
sanitized = sanitized.replace(/\0/g, '');
|
||||
|
||||
// Remove path traversal attempts
|
||||
sanitized = sanitized.replace(/\.\.\//g, '').replace(/\.\.\\/g, '');
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively sanitize all string values in an object
|
||||
*/
|
||||
export function sanitizeInput(input: any): any {
|
||||
if (typeof input === 'string') {
|
||||
// Supprimer les caractères dangereux
|
||||
return input.replace(/[<>]/g, '').trim();
|
||||
return sanitizeString(input);
|
||||
}
|
||||
|
||||
|
||||
if (Array.isArray(input)) {
|
||||
return input.map(item => sanitizeInput(item));
|
||||
}
|
||||
|
||||
if (typeof input === 'object' && input !== null) {
|
||||
const sanitized: any = {};
|
||||
for (const key in input) {
|
||||
sanitized[key] = sanitizeInput(input[key]);
|
||||
// Also sanitize object keys to prevent prototype pollution
|
||||
const sanitizedKey = sanitizeString(key);
|
||||
if (sanitizedKey === '__proto__' || sanitizedKey === 'constructor' || sanitizedKey === 'prototype') {
|
||||
continue; // Skip prototype pollution attempts
|
||||
}
|
||||
sanitized[sanitizedKey] = sanitizeInput(input[key]);
|
||||
}
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and sanitize email
|
||||
*/
|
||||
export function sanitizeEmail(email: string): string | null {
|
||||
if (!email || typeof email !== 'string') return null;
|
||||
|
||||
const normalized = validator.normalizeEmail(email);
|
||||
if (!normalized || !validator.isEmail(normalized)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and sanitize URL
|
||||
*/
|
||||
export function sanitizeUrl(url: string): string | null {
|
||||
if (!url || typeof url !== 'string') return null;
|
||||
|
||||
if (!validator.isURL(url, {
|
||||
protocols: ['http', 'https'],
|
||||
require_protocol: true,
|
||||
require_valid_protocol: true
|
||||
})) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for SQL injection patterns (for logging/monitoring)
|
||||
*/
|
||||
export function detectSqlInjection(input: string): boolean {
|
||||
if (typeof input !== 'string') return false;
|
||||
|
||||
const sqlPatterns = [
|
||||
/(\b(SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|TRUNCATE|EXEC|UNION|OR|AND)\b.*\b(FROM|INTO|TABLE|WHERE|SET)\b)/i,
|
||||
/(['"]?\s*(OR|AND)\s*['"]?\s*['"]?\s*=\s*['"]?)/i,
|
||||
/(--|\#|\/\*|\*\/)/,
|
||||
/(\bEXEC\b|\bEXECUTE\b|\bxp_)/i,
|
||||
];
|
||||
|
||||
return sqlPatterns.some(pattern => pattern.test(input));
|
||||
}
|
||||
|
||||
// Middleware de nettoyage des requêtes
|
||||
export function setupInputSanitization(app: Express) {
|
||||
app.use((req: Request, res: Response, next: NextFunction) => {
|
||||
// Log potential SQL injection attempts
|
||||
const checkAndLog = (data: any, source: string) => {
|
||||
if (typeof data === 'object' && data !== null) {
|
||||
for (const key in data) {
|
||||
const value = data[key];
|
||||
if (typeof value === 'string' && detectSqlInjection(value)) {
|
||||
console.warn(`🚨 Potential SQL injection detected in ${source}:`, {
|
||||
ip: req.ip,
|
||||
path: req.path,
|
||||
key,
|
||||
value: value.substring(0, 100) // Truncate for logging
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (req.body) {
|
||||
checkAndLog(req.body, 'body');
|
||||
req.body = sanitizeInput(req.body);
|
||||
}
|
||||
if (req.query) {
|
||||
checkAndLog(req.query, 'query');
|
||||
req.query = sanitizeInput(req.query);
|
||||
}
|
||||
if (req.params) {
|
||||
checkAndLog(req.params, 'params');
|
||||
req.params = sanitizeInput(req.params);
|
||||
}
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
// Middleware de logging sécurisé
|
||||
// ============================================================================
|
||||
// Secure Logging
|
||||
// ============================================================================
|
||||
|
||||
const SENSITIVE_KEYS = ['password', 'token', 'secret', 'apikey', 'api_key', 'authorization', 'cookie'];
|
||||
|
||||
export function secureLog(message: string, data?: any) {
|
||||
const timestamp = new Date().toISOString();
|
||||
const logData = data ? JSON.stringify(data, null, 2) : '';
|
||||
|
||||
// En production, ne pas logger les données sensibles
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
if (message.includes('password') || message.includes('token')) {
|
||||
console.log(`[${timestamp}] ${message} - [SENSITIVE DATA HIDDEN]`);
|
||||
} else {
|
||||
console.log(`[${timestamp}] ${message}`, logData);
|
||||
|
||||
// Mask sensitive data
|
||||
const maskSensitive = (obj: any): any => {
|
||||
if (!obj || typeof obj !== 'object') return obj;
|
||||
|
||||
const masked: any = Array.isArray(obj) ? [] : {};
|
||||
for (const key in obj) {
|
||||
if (SENSITIVE_KEYS.some(s => key.toLowerCase().includes(s))) {
|
||||
masked[key] = '[REDACTED]';
|
||||
} else if (typeof obj[key] === 'object') {
|
||||
masked[key] = maskSensitive(obj[key]);
|
||||
} else {
|
||||
masked[key] = obj[key];
|
||||
}
|
||||
}
|
||||
return masked;
|
||||
};
|
||||
|
||||
const logData = data ? JSON.stringify(maskSensitive(data), null, 2) : '';
|
||||
|
||||
// Check message for sensitive content
|
||||
if (SENSITIVE_KEYS.some(s => message.toLowerCase().includes(s))) {
|
||||
console.log(`[${timestamp}] ${message} - [SENSITIVE DATA HIDDEN]`);
|
||||
} else {
|
||||
console.log(`[${timestamp}] ${message}`, logData);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user