feat: add event slugs to prevent ID enumeration attacks

Replace sequential event IDs in URLs with unique alphanumeric slugs to prevent enumeration attacks. Event URLs now use format /events/{slug}/chat instead of /events/{id}/chat.

Backend changes:
- Add slug field (VARCHAR 50, unique) to Event model
- Create migration with auto-generated 12-char MD5-based slugs for existing events
- Update GET /api/events/:slug endpoint (changed from :id)
- Update GET /api/events/:slug/messages endpoint (changed from :eventId)
- Modify Socket.IO join_event_room to accept slug parameter
- Update send_event_message to use stored event context instead of passing eventId

Frontend changes:
- Update eventsAPI.getBySlug() method (changed from getById)
- Update eventsAPI.getMessages() to use slug parameter
- Change route from /events/:eventId/chat to /events/:slug/chat
- Update EventsPage to navigate using event.slug
- Update EventChatPage to fetch event data via slug and use slug in socket events

Security impact: Prevents attackers from discovering all events by iterating sequential IDs.
This commit is contained in:
Radosław Gierwiało
2025-11-13 21:43:58 +01:00
parent 20f405cab3
commit b2c2527c46
8 changed files with 127 additions and 37 deletions

View File

@@ -54,25 +54,38 @@ function initializeSocket(httpServer) {
console.log(`✅ User connected: ${socket.user.username} (${socket.id})`);
// Join event room
socket.on('join_event_room', async ({ eventId }) => {
socket.on('join_event_room', async ({ slug }) => {
try {
// Find event by slug
const event = await prisma.event.findUnique({
where: { slug },
select: { id: true, slug: true },
});
if (!event) {
socket.emit('error', { message: 'Event not found' });
return;
}
const eventId = event.id;
const roomName = `event_${eventId}`;
socket.join(roomName);
socket.currentEventRoom = roomName;
socket.currentEventId = eventId;
socket.currentEventSlug = slug;
// Record event participation in database
await prisma.eventParticipant.upsert({
where: {
userId_eventId: {
userId: socket.user.id,
eventId: parseInt(eventId),
eventId: eventId,
},
},
update: {}, // Don't update anything if already exists
create: {
userId: socket.user.id,
eventId: parseInt(eventId),
eventId: eventId,
},
});
@@ -90,7 +103,7 @@ function initializeSocket(httpServer) {
activeUsers.get(eventId).add(JSON.stringify(userInfo));
console.log(`👤 ${socket.user.username} joined event room ${eventId}`);
console.log(`👤 ${socket.user.username} joined event room ${slug} (ID: ${eventId})`);
// Load last 20 messages from database
const chatRoom = await prisma.chatRoom.findFirst({
@@ -146,8 +159,13 @@ function initializeSocket(httpServer) {
});
// Leave event room
socket.on('leave_event_room', ({ eventId }) => {
const roomName = `event_${eventId}`;
socket.on('leave_event_room', () => {
if (!socket.currentEventId || !socket.currentEventRoom) {
return;
}
const eventId = socket.currentEventId;
const roomName = socket.currentEventRoom;
socket.leave(roomName);
// Remove from active users
@@ -166,18 +184,28 @@ function initializeSocket(httpServer) {
io.to(roomName).emit('active_users', updatedUsers);
}
console.log(`👤 ${socket.user.username} left event room ${eventId}`);
console.log(`👤 ${socket.user.username} left event room ${socket.currentEventSlug}`);
// Clear current event data
socket.currentEventId = null;
socket.currentEventRoom = null;
socket.currentEventSlug = null;
});
// Send message to event room
socket.on('send_event_message', async ({ eventId, content }) => {
socket.on('send_event_message', async ({ content }) => {
try {
const roomName = `event_${eventId}`;
if (!socket.currentEventId || !socket.currentEventRoom) {
return socket.emit('error', { message: 'Not in an event room' });
}
const eventId = socket.currentEventId;
const roomName = socket.currentEventRoom;
// Save message to database
const chatRoom = await prisma.chatRoom.findFirst({
where: {
eventId: parseInt(eventId),
eventId: eventId,
type: 'event',
},
});
@@ -216,7 +244,7 @@ function initializeSocket(httpServer) {
createdAt: message.createdAt,
});
console.log(`💬 Message in event ${eventId} from ${socket.user.username}`);
console.log(`💬 Message in event ${socket.currentEventSlug} from ${socket.user.username}`);
} catch (error) {
console.error('Send message error:', error);
socket.emit('error', { message: 'Failed to send message' });