feat: implement critical security fixes for SQL injection and authentication vulnerabilities
Test Gitea Actions / test (push) Successful in 20s
continuous-integration/drone/push Build is passing

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 <[email protected]>
This commit is contained in:
2025-07-13 00:40:05 +02:00
co-authored by Claude
parent 0d33f21ee6
commit e35ac6a41c
36 changed files with 4152 additions and 20 deletions
+34 -15
View File
@@ -1,6 +1,7 @@
const express = require('express');
const { db } = require('../database/db');
const { adminAuth } = require('../middleware/auth');
const { sanitizeDays, addDateRangeCondition } = require('../utils/sqlSecurity');
const router = express.Router();
// Get dashboard statistics
@@ -14,11 +15,15 @@ router.get('/stats', adminAuth, async (req, res) => {
.first();
// Get events expiring within 7 days
const sevenDaysFromNow = new Date();
sevenDaysFromNow.setDate(sevenDaysFromNow.getDate() + 7);
const now = new Date();
const expiringEvents = await db('events')
.where('is_active', true)
.where('is_archived', false)
.whereRaw('expires_at <= datetime("now", "+7 days")')
.whereRaw('expires_at > datetime("now")')
.where('expires_at', '<=', sevenDaysFromNow.toISOString())
.where('expires_at', '>', now.toISOString())
.count('id as count')
.first();
@@ -33,16 +38,19 @@ router.get('/stats', adminAuth, async (req, res) => {
.first();
// Get total views (last 30 days)
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
const totalViews = await db('access_logs')
.where('action', 'view')
.whereRaw('timestamp >= datetime("now", "-30 days")')
.where('timestamp', '>=', thirtyDaysAgo.toISOString())
.count('id as count')
.first();
// Get total downloads (last 30 days)
const totalDownloads = await db('access_logs')
.where('action', 'download')
.whereRaw('timestamp >= datetime("now", "-30 days")')
.where('timestamp', '>=', thirtyDaysAgo.toISOString())
.count('id as count')
.first();
@@ -53,17 +61,20 @@ router.get('/stats', adminAuth, async (req, res) => {
.first();
// Calculate trends (compare with previous 30 days)
const sixtyDaysAgo = new Date();
sixtyDaysAgo.setDate(sixtyDaysAgo.getDate() - 60);
const previousViews = await db('access_logs')
.where('action', 'view')
.whereRaw('timestamp >= datetime("now", "-60 days")')
.whereRaw('timestamp < datetime("now", "-30 days")')
.where('timestamp', '>=', sixtyDaysAgo.toISOString())
.where('timestamp', '<', thirtyDaysAgo.toISOString())
.count('id as count')
.first();
const previousDownloads = await db('access_logs')
.where('action', 'download')
.whereRaw('timestamp >= datetime("now", "-60 days")')
.whereRaw('timestamp < datetime("now", "-30 days")')
.where('timestamp', '>=', sixtyDaysAgo.toISOString())
.where('timestamp', '<', thirtyDaysAgo.toISOString())
.count('id as count')
.first();
@@ -140,9 +151,12 @@ router.get('/health', adminAuth, async (req, res) => {
.where('status', 'pending')
.count('* as count');
const twentyFourHoursAgo = new Date();
twentyFourHoursAgo.setHours(twentyFourHoursAgo.getHours() - 24);
const [failedEmails] = await db('email_queue')
.where('status', 'failed')
.whereRaw('created_at >= datetime("now", "-24 hours")')
.where('created_at', '>=', twentyFourHoursAgo.toISOString())
.count('* as count');
const emailStatus = failedEmails.count > 10 ? 'warning' : 'healthy';
@@ -194,7 +208,7 @@ router.get('/health', adminAuth, async (req, res) => {
// Get analytics data for charts
router.get('/analytics', adminAuth, async (req, res) => {
try {
const days = parseInt(req.query.days) || 7;
const days = sanitizeDays(req.query.days || 7);
// Generate date range
const dates = [];
@@ -207,24 +221,29 @@ router.get('/analytics', adminAuth, async (req, res) => {
});
}
// Calculate the start date for queries
const startDate = new Date();
startDate.setDate(startDate.getDate() - days);
const startDateStr = startDate.toISOString();
// Get views per day
const viewsData = await db('access_logs')
.select(db.raw('DATE(timestamp) as date'), db.raw('COUNT(*) as count'))
.where('action', 'view')
.whereRaw(`timestamp >= datetime("now", "-${days} days")`)
.where('timestamp', '>=', startDateStr)
.groupByRaw('DATE(timestamp)');
// Get downloads per day
const downloadsData = await db('access_logs')
.select(db.raw('DATE(timestamp) as date'), db.raw('COUNT(*) as count'))
.where('action', 'download')
.whereRaw(`timestamp >= datetime("now", "-${days} days")`)
.where('timestamp', '>=', startDateStr)
.groupByRaw('DATE(timestamp)');
// Get unique visitors per day
const visitorsData = await db('access_logs')
.select(db.raw('DATE(timestamp) as date'), db.raw('COUNT(DISTINCT ip_address) as count'))
.whereRaw(`timestamp >= datetime("now", "-${days} days")`)
.where('timestamp', '>=', startDateStr)
.groupByRaw('DATE(timestamp)');
// Merge data into dates array
@@ -249,7 +268,7 @@ router.get('/analytics', adminAuth, async (req, res) => {
.select(db.raw('COUNT(*) as views'))
.join('events', 'access_logs.event_id', 'events.id')
.where('access_logs.action', 'view')
.whereRaw(`access_logs.timestamp >= datetime("now", "-${days} days")`)
.where('access_logs.timestamp', '>=', startDateStr)
.groupBy('events.id')
.orderBy('views', 'desc')
.limit(5);
@@ -266,7 +285,7 @@ router.get('/analytics', adminAuth, async (req, res) => {
`),
db.raw('COUNT(*) as count')
)
.whereRaw(`timestamp >= datetime("now", "-${days} days")`)
.where('timestamp', '>=', startDateStr)
.groupBy('device_type');
const totalDevices = deviceData.reduce((sum, d) => sum + d.count, 0);