From d0f118e7af0b2ede517d6d051226c38fa8e557b6 Mon Sep 17 00:00:00 2001 From: Lauri Ojansivu Date: Fri, 10 Oct 2025 22:09:27 +0300 Subject: [PATCH] Security Fix: Computational Resource Abuse in Export endpoints. Thanks to Anynymous Security Researcher and xet7 ! --- models/export.js | 110 +++++++++++++++++++++++++++++++++++++++++- models/exportExcel.js | 26 ++++++++++ models/exportPDF.js | 26 ++++++++++ 3 files changed, 160 insertions(+), 2 deletions(-) diff --git a/models/export.js b/models/export.js index 68b74087b..a45f8dc77 100644 --- a/models/export.js +++ b/models/export.js @@ -32,8 +32,37 @@ if (Meteor.isServer) { let user = null; let impersonateDone = false; let adminId = null; + + // First check if board exists and is public to avoid unnecessary authentication + const board = ReactiveCache.getBoard(boardId); + if (!board) { + JsonRoutes.sendResult(res, 404); + return; + } + + // If board is public, skip expensive authentication operations + if (board.isPublic()) { + // Public boards don't require authentication - skip hash operations + const exporter = new Exporter(boardId); + JsonRoutes.sendResult(res, { + code: 200, + data: exporter.build(), + }); + return; + } + + // Only perform expensive authentication for private boards const loginToken = req.query.authToken; if (loginToken) { + // Validate token length to prevent resource abuse + if (loginToken.length > 10000) { + if (process.env.DEBUG === 'true') { + console.warn('Suspiciously long auth token received, rejecting to prevent resource abuse'); + } + JsonRoutes.sendResult(res, 400); + return; + } + const hashToken = Accounts._hashLoginToken(loginToken); user = ReactiveCache.getUser({ 'services.resume.loginTokens.hashedToken': hashToken, @@ -44,6 +73,7 @@ if (Meteor.isServer) { Authentication.checkUserId(req.userId); user = ReactiveCache.getUser({ _id: req.userId, isAdmin: true }); } + const exporter = new Exporter(boardId); if (exporter.canExport(user) || impersonateDone) { if (impersonateDone) { @@ -94,8 +124,37 @@ if (Meteor.isServer) { let user = null; let impersonateDone = false; let adminId = null; + + // First check if board exists and is public to avoid unnecessary authentication + const board = ReactiveCache.getBoard(boardId); + if (!board) { + JsonRoutes.sendResult(res, 404); + return; + } + + // If board is public, skip expensive authentication operations + if (board.isPublic()) { + // Public boards don't require authentication - skip hash operations + const exporter = new Exporter(boardId, attachmentId); + JsonRoutes.sendResult(res, { + code: 200, + data: exporter.build(), + }); + return; + } + + // Only perform expensive authentication for private boards const loginToken = req.query.authToken; if (loginToken) { + // Validate token length to prevent resource abuse + if (loginToken.length > 10000) { + if (process.env.DEBUG === 'true') { + console.warn('Suspiciously long auth token received, rejecting to prevent resource abuse'); + } + JsonRoutes.sendResult(res, 400); + return; + } + const hashToken = Accounts._hashLoginToken(loginToken); user = ReactiveCache.getUser({ 'services.resume.loginTokens.hashedToken': hashToken, @@ -106,6 +165,7 @@ if (Meteor.isServer) { Authentication.checkUserId(req.userId); user = ReactiveCache.getUser({ _id: req.userId, isAdmin: true }); } + const exporter = new Exporter(boardId, attachmentId); if (exporter.canExport(user) || impersonateDone) { if (impersonateDone) { @@ -148,8 +208,53 @@ if (Meteor.isServer) { let user = null; let impersonateDone = false; let adminId = null; + + // First check if board exists and is public to avoid unnecessary authentication + const board = ReactiveCache.getBoard(boardId); + if (!board) { + res.writeHead(404); + res.end('Board not found'); + return; + } + + // If board is public, skip expensive authentication operations + if (board.isPublic()) { + // Public boards don't require authentication - skip hash operations + const exporter = new Exporter(boardId); + + if( params.query.delimiter == "\t" ) { + // TSV file + res.writeHead(200, { + 'Content-Type': 'text/tsv', + }); + } + else { + // CSV file (comma or semicolon) + res.writeHead(200, { + 'Content-Type': 'text/csv; charset=utf-8', + }); + // Adding UTF8 BOM to quick fix MS Excel issue + // use Uint8Array to prevent from converting bytes to string + res.write(new Uint8Array([0xEF, 0xBB, 0xBF])); + } + res.write(exporter.buildCsv(params.query.delimiter, 'en')); + res.end(); + return; + } + + // Only perform expensive authentication for private boards const loginToken = params.query.authToken; if (loginToken) { + // Validate token length to prevent resource abuse + if (loginToken.length > 10000) { + if (process.env.DEBUG === 'true') { + console.warn('Suspiciously long auth token received, rejecting to prevent resource abuse'); + } + res.writeHead(400); + res.end('Invalid token'); + return; + } + const hashToken = Accounts._hashLoginToken(loginToken); user = ReactiveCache.getUser({ 'services.resume.loginTokens.hashedToken': hashToken, @@ -163,6 +268,7 @@ if (Meteor.isServer) { isAdmin: true, }); } + const exporter = new Exporter(boardId); if (exporter.canExport(user) || impersonateDone) { if (impersonateDone) { @@ -176,12 +282,12 @@ if (Meteor.isServer) { reason: exportType, }); } - + let userLanguage = 'en'; if (user && user.profile) { userLanguage = user.profile.language } - + if( params.query.delimiter == "\t" ) { // TSV file res.writeHead(200, { diff --git a/models/exportExcel.js b/models/exportExcel.js index ffec16e85..598fb92d6 100644 --- a/models/exportExcel.js +++ b/models/exportExcel.js @@ -35,8 +35,34 @@ runOnServer(function() { let user = null; let impersonateDone = false; let adminId = null; + + // First check if board exists and is public to avoid unnecessary authentication + const board = ReactiveCache.getBoard(boardId); + if (!board) { + res.end('Board not found'); + return; + } + + // If board is public, skip expensive authentication operations + if (board.isPublic()) { + // Public boards don't require authentication - skip hash operations + const exporterExcel = new ExporterExcel(boardId, 'en'); + exporterExcel.build(res); + return; + } + + // Only perform expensive authentication for private boards const loginToken = params.query.authToken; if (loginToken) { + // Validate token length to prevent resource abuse + if (loginToken.length > 10000) { + if (process.env.DEBUG === 'true') { + console.warn('Suspiciously long auth token received, rejecting to prevent resource abuse'); + } + res.end('Invalid token'); + return; + } + const hashToken = Accounts._hashLoginToken(loginToken); user = ReactiveCache.getUser({ 'services.resume.loginTokens.hashedToken': hashToken, diff --git a/models/exportPDF.js b/models/exportPDF.js index 0af6e73f2..272651f98 100644 --- a/models/exportPDF.js +++ b/models/exportPDF.js @@ -37,8 +37,34 @@ runOnServer(function() { let user = null; let impersonateDone = false; let adminId = null; + + // First check if board exists and is public to avoid unnecessary authentication + const board = ReactiveCache.getBoard(boardId); + if (!board) { + res.end('Board not found'); + return; + } + + // If board is public, skip expensive authentication operations + if (board.isPublic()) { + // Public boards don't require authentication - skip hash operations + const exporterCardPDF = new ExporterCardPDF(boardId); + exporterCardPDF.build(res); + return; + } + + // Only perform expensive authentication for private boards const loginToken = params.query.authToken; if (loginToken) { + // Validate token length to prevent resource abuse + if (loginToken.length > 10000) { + if (process.env.DEBUG === 'true') { + console.warn('Suspiciously long auth token received, rejecting to prevent resource abuse'); + } + res.end('Invalid token'); + return; + } + const hashToken = Accounts._hashLoginToken(loginToken); user = ReactiveCache.getUser({ 'services.resume.loginTokens.hashedToken': hashToken,