feat: implement critical security fixes for SQL injection and authentication vulnerabilities
Security Enhancements: - Fix SQL injection vulnerabilities by replacing whereRaw queries with parameterized queries - Add LIKE pattern escaping to prevent SQL injection in search functionality - Implement account lockout protection (5 failed attempts = 30 min lockout) - Add comprehensive login attempt tracking and audit trail - Enhance JWT tokens with issuer validation, IP tracking, and password change detection - Add logout endpoint and session management - Prevent user enumeration with generic error messages Database Changes: - Add login_attempts table for authentication tracking - Add security columns to admin_users (password_changed_at, last_login_ip, two_factor_enabled) New Security Features: - Brute force protection with configurable lockout duration - Automatic cleanup of old login attempts - Enhanced authentication middleware with stricter validation - Monitoring scripts for security health checks All fixes are backward compatible and production-ready with rollback plans included. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Test script to verify routes work correctly after SQL security fixes
|
||||
* Run this before deploying to production
|
||||
*/
|
||||
|
||||
const request = require('supertest');
|
||||
const app = require('../src/app');
|
||||
const { db } = require('../src/database/db');
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
// Generate admin token for testing
|
||||
const adminToken = jwt.sign(
|
||||
{ id: 1, username: 'admin', role: 'admin' },
|
||||
process.env.JWT_SECRET || 'test-secret'
|
||||
);
|
||||
|
||||
console.log('=== Testing Routes After SQL Security Fixes ===\n');
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
async function testRoute(description, testFn) {
|
||||
try {
|
||||
await testFn();
|
||||
console.log(`✅ ${description}`);
|
||||
passed++;
|
||||
} catch (error) {
|
||||
console.log(`❌ ${description}`);
|
||||
console.error(` Error: ${error.message}`);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
async function runTests() {
|
||||
// Test Dashboard Stats (uses whereRaw fixes)
|
||||
await testRoute('Dashboard stats endpoint', async () => {
|
||||
const res = await request(app)
|
||||
.get('/api/admin/dashboard/stats')
|
||||
.set('Authorization', `Bearer ${adminToken}`)
|
||||
.expect(200);
|
||||
|
||||
if (!res.body.hasOwnProperty('activeEvents')) {
|
||||
throw new Error('Missing activeEvents in response');
|
||||
}
|
||||
});
|
||||
|
||||
// Test Analytics with days parameter (uses sanitizeDays)
|
||||
await testRoute('Analytics with valid days parameter', async () => {
|
||||
const res = await request(app)
|
||||
.get('/api/admin/dashboard/analytics?days=7')
|
||||
.set('Authorization', `Bearer ${adminToken}`)
|
||||
.expect(200);
|
||||
|
||||
if (!res.body.chartData || res.body.chartData.length !== 7) {
|
||||
throw new Error('Invalid chart data');
|
||||
}
|
||||
});
|
||||
|
||||
// Test Analytics with SQL injection attempt in days
|
||||
await testRoute('Analytics rejects SQL injection in days parameter', async () => {
|
||||
const res = await request(app)
|
||||
.get('/api/admin/dashboard/analytics?days=7; DROP TABLE events; --')
|
||||
.set('Authorization', `Bearer ${adminToken}`)
|
||||
.expect(200);
|
||||
|
||||
// Should default to 7 days
|
||||
if (res.body.chartData.length !== 7) {
|
||||
throw new Error('Days parameter not properly sanitized');
|
||||
}
|
||||
});
|
||||
|
||||
// Test Event search with normal text (uses escapeLikePattern)
|
||||
await testRoute('Event search with normal text', async () => {
|
||||
const res = await request(app)
|
||||
.get('/api/admin/events?search=test')
|
||||
.set('Authorization', `Bearer ${adminToken}`)
|
||||
.expect(200);
|
||||
|
||||
if (!res.body.hasOwnProperty('events')) {
|
||||
throw new Error('Missing events in response');
|
||||
}
|
||||
});
|
||||
|
||||
// Test Event search with special characters
|
||||
await testRoute('Event search with special characters', async () => {
|
||||
const res = await request(app)
|
||||
.get('/api/admin/events?search=50%_test')
|
||||
.set('Authorization', `Bearer ${adminToken}`)
|
||||
.expect(200);
|
||||
|
||||
// Should handle special chars safely
|
||||
if (!res.body.hasOwnProperty('events')) {
|
||||
throw new Error('Failed to handle special characters');
|
||||
}
|
||||
});
|
||||
|
||||
// Test Event search with SQL injection attempt
|
||||
await testRoute('Event search prevents SQL injection', async () => {
|
||||
const res = await request(app)
|
||||
.get("/api/admin/events?search=' OR 1=1 --")
|
||||
.set('Authorization', `Bearer ${adminToken}`)
|
||||
.expect(200);
|
||||
|
||||
// Should return empty results, not all events
|
||||
if (!res.body.hasOwnProperty('events')) {
|
||||
throw new Error('SQL injection may not be prevented');
|
||||
}
|
||||
});
|
||||
|
||||
// Test Photo search (if event exists)
|
||||
await testRoute('Photo search functionality', async () => {
|
||||
// First check if we have any events
|
||||
const event = await db('events').first();
|
||||
if (event) {
|
||||
const res = await request(app)
|
||||
.get(`/api/admin/events/${event.id}/photos?search=test`)
|
||||
.set('Authorization', `Bearer ${adminToken}`)
|
||||
.expect(200);
|
||||
|
||||
if (!res.body.hasOwnProperty('photos')) {
|
||||
throw new Error('Missing photos in response');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Test Activity endpoint
|
||||
await testRoute('Activity log endpoint', async () => {
|
||||
const res = await request(app)
|
||||
.get('/api/admin/dashboard/activity?limit=10')
|
||||
.set('Authorization', `Bearer ${adminToken}`)
|
||||
.expect(200);
|
||||
|
||||
if (!Array.isArray(res.body)) {
|
||||
throw new Error('Activity should return array');
|
||||
}
|
||||
});
|
||||
|
||||
// Test Health endpoint
|
||||
await testRoute('Health check endpoint', async () => {
|
||||
const res = await request(app)
|
||||
.get('/api/admin/dashboard/health')
|
||||
.set('Authorization', `Bearer ${adminToken}`)
|
||||
.expect(200);
|
||||
|
||||
if (!res.body.hasOwnProperty('overall')) {
|
||||
throw new Error('Missing overall health status');
|
||||
}
|
||||
});
|
||||
|
||||
// Summary
|
||||
console.log('\n=== Test Summary ===');
|
||||
console.log(`Total tests: ${passed + failed}`);
|
||||
console.log(`Passed: ${passed}`);
|
||||
console.log(`Failed: ${failed}`);
|
||||
|
||||
if (failed === 0) {
|
||||
console.log('\n✅ All route tests passed! Safe to deploy.');
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.log('\n❌ Some tests failed. Review the fixes before deploying.');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Run tests
|
||||
runTests().catch(error => {
|
||||
console.error('Test runner error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user