feat(security): ameliorations securite - CSRF, sanitization validator.js, eval removal

This commit is contained in:
Michael committed 2026-01-12 14:29:02 +01:00
1 parent 78adc2c5ae
commit 0f8920f326
5 files changed
+342 -35

No files matched your search

+60
View File
@@ -42,10 +42,12 @@
"@tanstack/react-query": "^5.60.5",
"@types/bcrypt": "^6.0.0",
"@types/busboy": "^1.5.4",
"@types/cookie-parser": "^1.4.10",
"@types/form-data": "^2.2.1",
"@types/memoizee": "^0.4.12",
"@types/multer": "^2.0.0",
"@types/pg": "^8.15.4",
"@types/validator": "^13.15.10",
"bcrypt": "^6.0.0",
"busboy": "^1.6.0",
"caniuse-lite": "^1.0.30001727",
@@ -53,6 +55,8 @@
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"connect-pg-simple": "^10.0.0",
"cookie-parser": "^1.4.7",
"csrf-csrf": "^4.0.3",
"date-fns": "^3.6.0",
"drizzle-orm": "^0.39.1",
"drizzle-zod": "^0.7.0",
@@ -88,6 +92,7 @@
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7",
"tw-animate-css": "^1.2.5",
"validator": "^13.15.26",
"vaul": "^1.1.2",
"wouter": "^3.3.5",
"ws": "^8.18.0",
@@ -3658,6 +3663,15 @@
"@types/pg": "*"
}
},
"node_modules/@types/cookie-parser": {
"version": "1.4.10",
"resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.10.tgz",
"integrity": "sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==",
"license": "MIT",
"peerDependencies": {
"@types/express": "*"
}
},
"node_modules/@types/d3-array": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz",
@@ -3930,6 +3944,12 @@
"@types/send": "*"
}
},
"node_modules/@types/validator": {
"version": "13.15.10",
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz",
"integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==",
"license": "MIT"
},
"node_modules/@types/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
@@ -4543,6 +4563,28 @@
"node": ">= 0.6"
}
},
"node_modules/cookie-parser": {
"version": "1.4.7",
"resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz",
"integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==",
"license": "MIT",
"dependencies": {
"cookie": "0.7.2",
"cookie-signature": "1.0.6"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/cookie-parser/node_modules/cookie": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
@@ -4596,6 +4638,15 @@
"node": ">= 8"
}
},
"node_modules/csrf-csrf": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/csrf-csrf/-/csrf-csrf-4.0.3.tgz",
"integrity": "sha512-DaygOzelL4Qo1pHwI9LPyZL+X2456/OzpT596kNeZGiTSqKVDOk/9PPJ+FjzZacjMUEusOHw3WJKe1RW4iUhrw==",
"license": "ISC",
"dependencies": {
"http-errors": "^2.0.0"
}
},
"node_modules/cssesc": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
@@ -9127,6 +9178,15 @@
"node": ">= 0.4.0"
}
},
"node_modules/validator": {
"version": "13.15.26",
"resolved": "https://registry.npmjs.org/validator/-/validator-13.15.26.tgz",
"integrity": "sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==",
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+5
View File
@@ -44,10 +44,12 @@
"@tanstack/react-query": "^5.60.5",
"@types/bcrypt": "^6.0.0",
"@types/busboy": "^1.5.4",
"@types/cookie-parser": "^1.4.10",
"@types/form-data": "^2.2.1",
"@types/memoizee": "^0.4.12",
"@types/multer": "^2.0.0",
"@types/pg": "^8.15.4",
"@types/validator": "^13.15.10",
"bcrypt": "^6.0.0",
"busboy": "^1.6.0",
"caniuse-lite": "^1.0.30001727",
@@ -55,6 +57,8 @@
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"connect-pg-simple": "^10.0.0",
"cookie-parser": "^1.4.7",
"csrf-csrf": "^4.0.3",
"date-fns": "^3.6.0",
"drizzle-orm": "^0.39.1",
"drizzle-zod": "^0.7.0",
@@ -90,6 +94,7 @@
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7",
"tw-animate-css": "^1.2.5",
"validator": "^13.15.26",
"vaul": "^1.1.2",
"wouter": "^3.3.5",
"ws": "^8.18.0",
+27
View File
@@ -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
View File
@@ -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
View File
@@ -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);
}