Skip to content

Commit 43d3793

Browse files
committed
feat: implement comprehensive security and validation system
Major security enhancements: ## Security Features - Rate limiting: 1 click per 100ms per user with client & server enforcement - DDoS protection with automatic blocking and exponential backoff - Session management with browser fingerprinting - Automated behavior detection to prevent bot attacks - Comprehensive input validation and sanitization - Server-authoritative business logic with zero client-side trust ## New Security Infrastructure - Enhanced Convex schema with security tracking tables: - userSessions: tracks user activity and violations - securityEvents: logs all security incidents - requestAudit: complete audit trail of all requests - Security middleware with input validation functions - Admin dashboard queries for monitoring security events - Session-based rate limiting with violation tracking ## Client-Side Security - SessionManager utility for client-side rate limiting - Browser fingerprinting for session uniqueness - Client-side validation before server requests - Enhanced error handling for security responses ## Server-Side Security - validateAndEnforceSecurity middleware for all mutations - Atomic counter operations with comprehensive validation - Exponential backoff for repeated violations (up to 24h blocks) - IP-based tracking and monitoring - Whitelist-based counter name validation - Range validation for counter values (-1M to +1M) ## Admin & Monitoring - Security event monitoring and statistics - Active session tracking and management - Blocked session management with admin controls - Automated cleanup of old security records - Real-time security metrics and violation tracking ## Attack Prevention - SQL injection prevention through input validation - XSS protection with character filtering - CSRF protection through session validation - Automated clicking/bot detection - Rapid request detection and blocking - Volume-based DDoS detection All business logic is now server-authoritative with comprehensive logging and monitoring capabilities.
1 parent dad5ad8 commit 43d3793

File tree

7 files changed

+1018
-17
lines changed

7 files changed

+1018
-17
lines changed

convex/_generated/api.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ import type {
1313
FilterApi,
1414
FunctionReference,
1515
} from "convex/server";
16+
import type * as admin from "../admin.js";
1617
import type * as counter from "../counter.js";
18+
import type * as security from "../security.js";
1719

1820
/**
1921
* A utility for referencing Convex functions in your app's API.
@@ -24,7 +26,9 @@ import type * as counter from "../counter.js";
2426
* ```
2527
*/
2628
declare const fullApi: ApiFromModules<{
29+
admin: typeof admin;
2730
counter: typeof counter;
31+
security: typeof security;
2832
}>;
2933
export declare const api: FilterApi<
3034
typeof fullApi,

convex/admin.ts

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
import { query, mutation } from "./_generated/server";
2+
import { v } from "convex/values";
3+
4+
// Security monitoring queries for admin dashboard
5+
export const getSecurityEvents = query({
6+
args: {
7+
limit: v.optional(v.number()),
8+
severity: v.optional(v.string()),
9+
eventType: v.optional(v.string()),
10+
resolved: v.optional(v.boolean()),
11+
},
12+
handler: async (ctx, args) => {
13+
if (args.severity) {
14+
const events = await ctx.db
15+
.query("securityEvents")
16+
.withIndex("by_severity", (q) => q.eq("severity", args.severity!))
17+
.order("desc")
18+
.take(args.limit || 100);
19+
return events.filter(event =>
20+
args.resolved === undefined || event.resolved === args.resolved
21+
);
22+
} else if (args.eventType) {
23+
const events = await ctx.db
24+
.query("securityEvents")
25+
.withIndex("by_event_type", (q) => q.eq("eventType", args.eventType!))
26+
.order("desc")
27+
.take(args.limit || 100);
28+
return events.filter(event =>
29+
args.resolved === undefined || event.resolved === args.resolved
30+
);
31+
} else {
32+
const events = await ctx.db
33+
.query("securityEvents")
34+
.withIndex("by_timestamp")
35+
.order("desc")
36+
.take(args.limit || 100);
37+
return events.filter(event =>
38+
args.resolved === undefined || event.resolved === args.resolved
39+
);
40+
}
41+
},
42+
});
43+
44+
export const getActiveSessions = query({
45+
args: { limit: v.optional(v.number()) },
46+
handler: async (ctx, args) => {
47+
const twentyFourHoursAgo = Date.now() - 24 * 60 * 60 * 1000;
48+
49+
return await ctx.db
50+
.query("userSessions")
51+
.withIndex("by_last_activity")
52+
.filter((q) => q.gt(q.field("lastActivity"), twentyFourHoursAgo))
53+
.order("desc")
54+
.take(args.limit || 50);
55+
},
56+
});
57+
58+
export const getBlockedSessions = query({
59+
args: {},
60+
handler: async (ctx) => {
61+
const currentTime = Date.now();
62+
63+
return await ctx.db
64+
.query("userSessions")
65+
.filter((q) =>
66+
q.and(
67+
q.eq(q.field("isBlocked"), true),
68+
q.gt(q.field("blockUntil"), currentTime)
69+
)
70+
)
71+
.collect();
72+
},
73+
});
74+
75+
export const getRequestAuditLogs = query({
76+
args: {
77+
sessionId: v.optional(v.string()),
78+
action: v.optional(v.string()),
79+
result: v.optional(v.string()),
80+
limit: v.optional(v.number()),
81+
},
82+
handler: async (ctx, args) => {
83+
if (args.sessionId) {
84+
return await ctx.db
85+
.query("requestAudit")
86+
.withIndex("by_session", (q) => q.eq("sessionId", args.sessionId!))
87+
.order("desc")
88+
.take(args.limit || 100);
89+
} else if (args.result) {
90+
return await ctx.db
91+
.query("requestAudit")
92+
.withIndex("by_result", (q) => q.eq("result", args.result!))
93+
.order("desc")
94+
.take(args.limit || 100);
95+
} else {
96+
return await ctx.db
97+
.query("requestAudit")
98+
.withIndex("by_timestamp")
99+
.order("desc")
100+
.take(args.limit || 100);
101+
}
102+
},
103+
});
104+
105+
// Security statistics
106+
export const getSecurityStats = query({
107+
args: {},
108+
handler: async (ctx) => {
109+
const twentyFourHoursAgo = Date.now() - 24 * 60 * 60 * 1000;
110+
const oneHourAgo = Date.now() - 60 * 60 * 1000;
111+
112+
// Get security events in last 24 hours
113+
const recentEvents = await ctx.db
114+
.query("securityEvents")
115+
.withIndex("by_timestamp")
116+
.filter((q) => q.gt(q.field("timestamp"), twentyFourHoursAgo))
117+
.collect();
118+
119+
// Get active sessions
120+
const activeSessions = await ctx.db
121+
.query("userSessions")
122+
.withIndex("by_last_activity")
123+
.filter((q) => q.gt(q.field("lastActivity"), oneHourAgo))
124+
.collect();
125+
126+
// Get blocked sessions
127+
const blockedSessions = await ctx.db
128+
.query("userSessions")
129+
.filter((q) =>
130+
q.and(
131+
q.eq(q.field("isBlocked"), true),
132+
q.gt(q.field("blockUntil"), Date.now())
133+
)
134+
)
135+
.collect();
136+
137+
// Categorize events by type and severity
138+
const eventsByType = recentEvents.reduce((acc, event) => {
139+
acc[event.eventType] = (acc[event.eventType] || 0) + 1;
140+
return acc;
141+
}, {} as Record<string, number>);
142+
143+
const eventsBySeverity = recentEvents.reduce((acc, event) => {
144+
acc[event.severity] = (acc[event.severity] || 0) + 1;
145+
return acc;
146+
}, {} as Record<string, number>);
147+
148+
return {
149+
totalEvents24h: recentEvents.length,
150+
activeSessionsLastHour: activeSessions.length,
151+
currentlyBlocked: blockedSessions.length,
152+
eventsByType,
153+
eventsBySeverity,
154+
topViolators: blockedSessions
155+
.sort((a, b) => b.violationCount - a.violationCount)
156+
.slice(0, 10)
157+
.map(session => ({
158+
sessionId: session.sessionId,
159+
violationCount: session.violationCount,
160+
blockUntil: session.blockUntil,
161+
ipAddress: session.ipAddress,
162+
})),
163+
};
164+
},
165+
});
166+
167+
// Admin actions
168+
export const resolveSecurityEvent = mutation({
169+
args: { eventId: v.id("securityEvents") },
170+
handler: async (ctx, args) => {
171+
await ctx.db.patch(args.eventId, { resolved: true });
172+
},
173+
});
174+
175+
export const unblockSession = mutation({
176+
args: { sessionId: v.string() },
177+
handler: async (ctx, args) => {
178+
const session = await ctx.db
179+
.query("userSessions")
180+
.withIndex("by_session", (q) => q.eq("sessionId", args.sessionId))
181+
.first();
182+
183+
if (session) {
184+
await ctx.db.patch(session._id, {
185+
isBlocked: false,
186+
blockUntil: undefined,
187+
violationCount: 0,
188+
});
189+
190+
// Log admin action
191+
await ctx.db.insert("securityEvents", {
192+
sessionId: args.sessionId,
193+
eventType: "admin_unblock",
194+
severity: "low",
195+
details: {
196+
action: "Session manually unblocked by admin",
197+
additionalData: `Admin unblocked session ${args.sessionId}`,
198+
},
199+
timestamp: Date.now(),
200+
resolved: true,
201+
});
202+
}
203+
},
204+
});
205+
206+
export const blockSession = mutation({
207+
args: {
208+
sessionId: v.string(),
209+
duration: v.number(), // in milliseconds
210+
reason: v.string(),
211+
},
212+
handler: async (ctx, args) => {
213+
const session = await ctx.db
214+
.query("userSessions")
215+
.withIndex("by_session", (q) => q.eq("sessionId", args.sessionId))
216+
.first();
217+
218+
if (session) {
219+
await ctx.db.patch(session._id, {
220+
isBlocked: true,
221+
blockUntil: Date.now() + args.duration,
222+
violationCount: session.violationCount + 1,
223+
});
224+
225+
// Log admin action
226+
await ctx.db.insert("securityEvents", {
227+
sessionId: args.sessionId,
228+
eventType: "admin_block",
229+
severity: "high",
230+
details: {
231+
action: "Session manually blocked by admin",
232+
additionalData: `Reason: ${args.reason}. Duration: ${args.duration}ms`,
233+
},
234+
timestamp: Date.now(),
235+
resolved: true,
236+
});
237+
}
238+
},
239+
});
240+
241+
// Cleanup old records (run periodically)
242+
export const cleanupOldRecords = mutation({
243+
args: { olderThanDays: v.number() },
244+
handler: async (ctx, args) => {
245+
const cutoffTime = Date.now() - (args.olderThanDays * 24 * 60 * 60 * 1000);
246+
247+
// Clean up old security events
248+
const oldEvents = await ctx.db
249+
.query("securityEvents")
250+
.withIndex("by_timestamp")
251+
.filter((q) => q.lt(q.field("timestamp"), cutoffTime))
252+
.collect();
253+
254+
for (const event of oldEvents) {
255+
await ctx.db.delete(event._id);
256+
}
257+
258+
// Clean up old audit logs
259+
const oldAudits = await ctx.db
260+
.query("requestAudit")
261+
.withIndex("by_timestamp")
262+
.filter((q) => q.lt(q.field("timestamp"), cutoffTime))
263+
.collect();
264+
265+
for (const audit of oldAudits) {
266+
await ctx.db.delete(audit._id);
267+
}
268+
269+
// Clean up inactive sessions
270+
const inactiveSessions = await ctx.db
271+
.query("userSessions")
272+
.withIndex("by_last_activity")
273+
.filter((q) => q.lt(q.field("lastActivity"), cutoffTime))
274+
.collect();
275+
276+
for (const session of inactiveSessions) {
277+
await ctx.db.delete(session._id);
278+
}
279+
280+
return {
281+
deletedEvents: oldEvents.length,
282+
deletedAudits: oldAudits.length,
283+
deletedSessions: inactiveSessions.length,
284+
};
285+
},
286+
});

0 commit comments

Comments
 (0)