Fix SECURITY ISSUE 5: Attachment API uses bearer value as userId and DoS (Low).

Thanks to Siam Thanat Hack (STH) and xet7 !
This commit is contained in:
Lauri Ojansivu 2025-11-02 11:42:07 +02:00
parent 0a1a075f31
commit ccd9034339
4 changed files with 312 additions and 11 deletions

View file

@ -1,4 +1,5 @@
import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
import { WebApp } from 'meteor/webapp';
import { ReactiveCache } from '/imports/reactiveCache';
import { Attachments, fileStoreStrategyFactory } from '/models/attachments';
@ -11,20 +12,24 @@ import { ObjectID } from 'bson';
// Attachment API HTTP routes
if (Meteor.isServer) {
// Helper function to authenticate API requests
// Helper function to authenticate API requests using X-User-Id and X-Auth-Token
function authenticateApiRequest(req) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
throw new Meteor.Error('unauthorized', 'Missing or invalid authorization header');
const userId = req.headers['x-user-id'];
const authToken = req.headers['x-auth-token'];
if (!userId || !authToken) {
throw new Meteor.Error('unauthorized', 'Missing X-User-Id or X-Auth-Token headers');
}
const token = authHeader.substring(7);
// Here you would validate the token and get the user ID
// For now, we'll use a simple approach - in production, you'd want proper JWT validation
const userId = token; // This should be replaced with proper token validation
if (!userId) {
throw new Meteor.Error('unauthorized', 'Invalid token');
// Hash the token and validate against stored login tokens
const hashedToken = Accounts._hashLoginToken(authToken);
const user = Meteor.users.findOne({
_id: userId,
'services.resume.loginTokens.hashedToken': hashedToken,
});
if (!user) {
throw new Meteor.Error('unauthorized', 'Invalid credentials');
}
return userId;
@ -47,15 +52,33 @@ if (Meteor.isServer) {
return next();
}
// Set timeout to prevent hanging connections
const timeout = setTimeout(() => {
if (!res.headersSent) {
sendErrorResponse(res, 408, 'Request timeout');
}
}, 30000); // 30 second timeout
try {
const userId = authenticateApiRequest(req);
let body = '';
let bodyComplete = false;
req.on('data', chunk => {
body += chunk.toString();
// Prevent excessive payload
if (body.length > 50 * 1024 * 1024) { // 50MB limit
req.connection.destroy();
clearTimeout(timeout);
}
});
req.on('end', () => {
if (bodyComplete) return; // Already processed
bodyComplete = true;
clearTimeout(timeout);
try {
const data = JSON.parse(body);
const { boardId, swimlaneId, listId, cardId, fileData, fileName, fileType, storageBackend } = data;
@ -154,7 +177,16 @@ if (Meteor.isServer) {
sendErrorResponse(res, 500, error.message);
}
});
req.on('error', (error) => {
clearTimeout(timeout);
if (!res.headersSent) {
console.error('Request error:', error);
sendErrorResponse(res, 400, 'Request error');
}
});
} catch (error) {
clearTimeout(timeout);
sendErrorResponse(res, 401, error.message);
}
});
@ -287,15 +319,31 @@ if (Meteor.isServer) {
return next();
}
const timeout = setTimeout(() => {
if (!res.headersSent) {
sendErrorResponse(res, 408, 'Request timeout');
}
}, 30000);
try {
const userId = authenticateApiRequest(req);
let body = '';
let bodyComplete = false;
req.on('data', chunk => {
body += chunk.toString();
if (body.length > 10 * 1024 * 1024) { // 10MB limit for metadata
req.connection.destroy();
clearTimeout(timeout);
}
});
req.on('end', () => {
if (bodyComplete) return;
bodyComplete = true;
clearTimeout(timeout);
try {
const data = JSON.parse(body);
const { attachmentId, targetBoardId, targetSwimlaneId, targetListId, targetCardId } = data;
@ -388,7 +436,16 @@ if (Meteor.isServer) {
sendErrorResponse(res, 500, error.message);
}
});
req.on('error', (error) => {
clearTimeout(timeout);
if (!res.headersSent) {
console.error('Request error:', error);
sendErrorResponse(res, 400, 'Request error');
}
});
} catch (error) {
clearTimeout(timeout);
sendErrorResponse(res, 401, error.message);
}
});
@ -399,15 +456,31 @@ if (Meteor.isServer) {
return next();
}
const timeout = setTimeout(() => {
if (!res.headersSent) {
sendErrorResponse(res, 408, 'Request timeout');
}
}, 30000);
try {
const userId = authenticateApiRequest(req);
let body = '';
let bodyComplete = false;
req.on('data', chunk => {
body += chunk.toString();
if (body.length > 10 * 1024 * 1024) {
req.connection.destroy();
clearTimeout(timeout);
}
});
req.on('end', () => {
if (bodyComplete) return;
bodyComplete = true;
clearTimeout(timeout);
try {
const data = JSON.parse(body);
const { attachmentId, targetBoardId, targetSwimlaneId, targetListId, targetCardId } = data;
@ -461,7 +534,16 @@ if (Meteor.isServer) {
sendErrorResponse(res, 500, error.message);
}
});
req.on('error', (error) => {
clearTimeout(timeout);
if (!res.headersSent) {
console.error('Request error:', error);
sendErrorResponse(res, 400, 'Request error');
}
});
} catch (error) {
clearTimeout(timeout);
sendErrorResponse(res, 401, error.message);
}
});