import { AutoReporter } from '../reporters/AutoReporter.js'; /** * TESTS SÉCURITÉ - API Controller * Tests de sécurité, injection, et validation */ const { describe, it, before, after } = require('node:test'); const assert = require('node:assert'); const http = require('node:http'); const { ManualServer } = require('../../lib/modes/ManualServer'); // Helper pour requêtes HTTP function makeRequest(options, postData = null) { return new Promise((resolve, reject) => { const req = http.request(options, (res) => { let data = ''; res.on('data', chunk => data += chunk); res.on('end', () => { try { const parsed = res.headers['content-type']?.includes('application/json') ? JSON.parse(data) : data; resolve({ statusCode: res.statusCode, headers: res.headers, data: parsed }); } catch (e) { resolve({ statusCode: res.statusCode, headers: res.headers, data }); } }); }); req.on('error', reject); req.setTimeout(5000, () => { req.destroy(); reject(new Error('Request timeout')); }); if (postData) { req.write(typeof postData === 'object' ? JSON.stringify(postData) : postData); } req.end(); }); } // Auto-Reporter Configuration const autoReporter = new AutoReporter(); describe('API Security Tests - Tests de Sécurité', () => { let server; let baseUrl; const testPort = 3097; before(async () => { server = new ManualServer({ port: testPort, wsPort: 8097 }); await server.start(); baseUrl = `http://localhost:${testPort}`; console.log(`🔒 Serveur sécurité démarré sur ${baseUrl}`); }); after(async () => { if (server) { await server.stop(); console.log('🛑 Serveur sécurité arrêté'); } }); describe('🛡️ Injection et XSS', () => { it('should handle SQL injection attempts safely', async () => { const sqlPayloads = [ "'; DROP TABLE projects; --", "1' OR '1'='1", "admin'/*", "1; SELECT * FROM users; --", "' UNION SELECT * FROM passwords --" ]; for (const payload of sqlPayloads) { const response = await makeRequest({ hostname: 'localhost', port: testPort, path: '/api/projects', method: 'POST', headers: { 'Content-Type': 'application/json' } }, { name: payload, description: 'Test injection SQL' }); // Doit traiter comme chaîne normale assert.ok([201, 400].includes(response.statusCode)); if (response.statusCode === 201) { assert.strictEqual(response.data.data.name, payload); assert.strictEqual(response.data.success, true); } } }); it('should handle XSS payloads safely', async () => { const xssPayloads = [ '', 'javascript:alert(1)', '', '', '">', "';alert(String.fromCharCode(88,83,83))//';alert(String.fromCharCode(88,83,83))//", '">', '' ]; for (const payload of xssPayloads) { const response = await makeRequest({ hostname: 'localhost', port: testPort, path: '/api/templates', method: 'POST', headers: { 'Content-Type': 'application/json' } }, { name: 'Test XSS', content: payload, description: 'Template avec payload XSS' }); assert.ok([201, 400].includes(response.statusCode)); if (response.statusCode === 201) { // Le payload doit être stocké tel quel, pas d'exécution assert.strictEqual(response.data.data.content, payload); } } }); it('should handle path traversal attempts', async () => { const pathTraversals = [ '../../../etc/passwd', '..\\..\\..\\windows\\system32\\config', '%2e%2e%2f%2e%2e%2f%2e%2e%2f', '....//....//....//etc/passwd', '..%2F..%2F..%2Fetc%2Fpasswd' ]; for (const path of pathTraversals) { try { const response = await makeRequest({ hostname: 'localhost', port: testPort, path: `/api/articles/${path}`, method: 'GET' }); // Doit retourner 404 ou 500, pas accéder aux fichiers système assert.ok([404, 500].includes(response.statusCode)); } catch (error) { // Erreurs d'URL malformée acceptables assert.ok(error.message.includes('Invalid') || error.message.includes('URI')); } } }); }); describe('🔍 Validation des Données', () => { it('should validate required fields strictly', async () => { const invalidPayloads = [ null, undefined, {}, { name: '' }, { name: null }, { name: undefined }, { description: 'Sans nom' }, { name: ' ' }, // Espaces seulement ]; for (const payload of invalidPayloads) { const response = await makeRequest({ hostname: 'localhost', port: testPort, path: '/api/projects', method: 'POST', headers: { 'Content-Type': 'application/json' } }, payload); assert.strictEqual(response.statusCode, 400); assert.strictEqual(response.data.success, false); assert.ok(response.data.error); } }); it('should handle malformed JSON gracefully', async () => { const malformedJsons = [ '{"name":}', '{"name":"test",}', '{name:"test"}', // Clés sans guillemets '{"name":"test"', // JSON incomplet 'null', 'undefined', '', '[]', // Array au lieu d'objet 'true', // Boolean au lieu d'objet '{"name":"test","config":{invalid}}' // Objet imbriqué malformé ]; for (const json of malformedJsons) { const response = await makeRequest({ hostname: 'localhost', port: testPort, path: '/api/projects', method: 'POST', headers: { 'Content-Type': 'application/json' } }, json); assert.strictEqual(response.statusCode, 400); assert.strictEqual(response.data.success, false); } }); it('should handle extreme data types', async () => { const extremePayloads = [ { name: 123 }, // Number au lieu de string { name: true }, // Boolean au lieu de string { name: [] }, // Array au lieu de string { name: {} }, // Object au lieu de string { name: 'Test', description: 123 }, // Number en description { name: 'Test', config: 'not an object' } // String au lieu d'object ]; for (const payload of extremePayloads) { const response = await makeRequest({ hostname: 'localhost', port: testPort, path: '/api/projects', method: 'POST', headers: { 'Content-Type': 'application/json' } }, payload); // L'API peut soit accepter (conversion automatique) soit rejeter assert.ok([201, 400].includes(response.statusCode)); if (response.statusCode === 201) { // Vérifier que les types sont convertis proprement assert.strictEqual(typeof response.data.data.name, 'string'); } } }); }); describe('🌐 Protocole HTTP', () => { it('should handle missing Content-Type header', async () => { const response = await makeRequest({ hostname: 'localhost', port: testPort, path: '/api/projects', method: 'POST' // Pas de Content-Type }, '{"name":"Test"}'); // Express devrait gérer gracieusement assert.ok([400, 415, 500].includes(response.statusCode)); }); it('should handle wrong Content-Type header', async () => { const response = await makeRequest({ hostname: 'localhost', port: testPort, path: '/api/projects', method: 'POST', headers: { 'Content-Type': 'text/plain' } }, '{"name":"Test"}'); assert.ok([400, 415, 500].includes(response.statusCode)); }); it('should handle unsupported HTTP methods', async () => { const methods = ['PATCH', 'DELETE', 'HEAD', 'TRACE']; for (const method of methods) { try { const response = await makeRequest({ hostname: 'localhost', port: testPort, path: '/api/projects', method }); // 405 Method Not Allowed ou 404 assert.ok([404, 405].includes(response.statusCode)); } catch (error) { // Certaines méthodes peuvent être rejetées par Node.js assert.ok(error.message.includes('Method') || error.message.includes('method')); } } }); it('should handle invalid URLs', async () => { const invalidPaths = [ '/api/projects with spaces', '/api/проекты', // Caractères non-ASCII '/api/projects?' + 'x'.repeat(1000), // Query string très longue '/api/projects#fragment' ]; for (const path of invalidPaths) { try { const response = await makeRequest({ hostname: 'localhost', port: testPort, path, method: 'GET' }); assert.ok([400, 404].includes(response.statusCode)); } catch (error) { // Erreurs de parsing d'URL attendues assert.ok(error.message.includes('Invalid') || error.message.includes('URI')); } } }); }); describe('📊 Limites et Performance', () => { it('should handle very large payloads', async () => { // Payload de 100KB const largePayload = { name: 'Large Test', description: 'X'.repeat(100 * 1024), // 100KB config: { data: 'Y'.repeat(10000) } }; const response = await makeRequest({ hostname: 'localhost', port: testPort, path: '/api/projects', method: 'POST', headers: { 'Content-Type': 'application/json' } }, largePayload); // Peut accepter ou rejeter selon les limites assert.ok([201, 400, 413].includes(response.statusCode)); }); it('should handle concurrent requests safely', async () => { // 10 requêtes simultanées const promises = Array(10).fill().map((_, i) => makeRequest({ hostname: 'localhost', port: testPort, path: '/api/projects', method: 'POST', headers: { 'Content-Type': 'application/json' } }, { name: `Concurrent Project ${i}`, description: `Test concurrence ${i}` }) ); const results = await Promise.allSettled(promises); // Vérifier succès const successes = results.filter(r => r.status === 'fulfilled' && r.value.statusCode === 201 ); assert.ok(successes.length > 0, 'Au moins une requête doit réussir'); // Vérifier unicité des IDs const ids = successes.map(r => r.value.data.data.id); const uniqueIds = new Set(ids); assert.strictEqual(ids.length, uniqueIds.size, 'IDs doivent être uniques'); }); it('should maintain stability under rapid requests', async () => { const requests = []; // 20 requêtes health check rapides for (let i = 0; i < 20; i++) { requests.push( makeRequest({ hostname: 'localhost', port: testPort, path: '/api/health', method: 'GET' }) ); } const results = await Promise.allSettled(requests); const successes = results.filter(r => r.status === 'fulfilled' && r.value.statusCode === 200 ); // Au moins 90% doivent réussir const successRate = (successes.length / results.length) * 100; assert.ok(successRate >= 90, `Success rate too low: ${successRate}%`); }); }); describe('🔒 Headers et CORS', () => { it('should handle CORS preflight requests', async () => { const response = await makeRequest({ hostname: 'localhost', port: testPort, path: '/api/health', method: 'OPTIONS', headers: { 'Origin': 'http://localhost:3000', 'Access-Control-Request-Method': 'POST' } }); // CORS devrait être géré assert.ok([200, 204].includes(response.statusCode)); }); it('should set appropriate security headers', async () => { const response = await makeRequest({ hostname: 'localhost', port: testPort, path: '/api/health', method: 'GET' }); assert.strictEqual(response.statusCode, 200); // Vérifier headers de sécurité de base assert.ok(response.headers['content-type']); // Les headers de sécurité peuvent être ajoutés par Express/middleware console.log('📋 Headers reçus:', Object.keys(response.headers)); }); }); }); console.log('🔒 Tests Sécurité API - Validation injection, XSS et limites');