Files
wekan/models/exportExcel.js
T
Lauri Ojansivu 1669a1af19 Fix 7 security issues reported by meifukun.
- Avatar localization redirect SSRF: fetch avatars via fetchSafe (DNS-pinned,
  redirects rejected) instead of native redirect:follow, and validate
  profile.avatarUrl scheme (http/https/data:image/local) at write time.
- Trello import source.url stored XSS: only render/store an http(s) activity
  source URL, so a javascript: board url can no longer execute on click.
- Trello LIVE import readable SSRF: gate every attachment/background/avatar
  download with validateAttachmentUrl, matching the offline import path.
- CAS login global user-data race: store validated CAS user data per credential
  token instead of a module-global _userData (fixes account-takeover race).
- Metrics X-Forwarded-For whitelist bypass: only trust XFF behind
  METRICS_TRUST_PROXY (parsed spoof-resistantly from the right); else use the
  socket peer address.
- Stale impersonation export bypass: remove the "|| impersonateDone" export
  authorization bypass; require real board visibility (canExport) only.
- Invitation code brute force: generate invitation codes with a 128-bit CSPRNG
  (crypto.randomBytes) instead of a 6-digit Math.random value, and rate-limit
  createUser (DDPRateLimiter).

The OIDC shared serviceData race from the same report was already fixed (#4897,
the per-login objects are callout-local). Source-guard regression test added
(tests/securityMeifukun.test.cjs) and wired into test:unit:node.

Thanks to meifukun and xet7 !
2026-07-19 12:22:56 +03:00

109 lines
3.8 KiB
JavaScript

import { Meteor } from 'meteor/meteor';
import { ReactiveCache } from '/imports/reactiveCache';
import { TAPi18n } from '/imports/i18n';
import { runOnServer } from './runOnServer';
import ImpersonatedUsers from '/models/impersonatedUsers';
runOnServer(function() {
// the ExporterExcel class is only available on server and in order to import
// it here we use runOnServer to have it inside a function instead of an
// if (Meteor.isServer) block
const { ExporterExcel } = require('./server/ExporterExcel');
const { WebApp } = require('meteor/webapp');
const { Authentication } = require('/server/authentication');
// todo XXX once we have a real API in place, move that route there
// todo XXX also share the route definition between the client and the server
// so that we could use something like
// `ApiRoutes.path('boards/exportExcel', boardId)``
// on the client instead of copy/pasting the route path manually between the
// client and the server.
/**
* @operation exportExcel
* @tag Boards
*
* @summary This route is used to export the board Excel.
*
* @description If user is already logged-in, pass loginToken as param
* "authToken": '/api/boards/:boardId/exportExcel?authToken=:token'
*
* See https://blog.kayla.com.au/server-side-route-authentication-in-meteor/
* for detailed explanations
*
* @param {string} boardId the ID of the board we are exporting
* @param {string} authToken the loginToken
*/
WebApp.handlers.get('/api/boards/:boardId/exportExcel', async function (req, res) {
const boardId = req.params.boardId;
let user = null;
let impersonateDone = false;
let adminId = null;
// First check if board exists and is public to avoid unnecessary authentication
const board = await 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 = 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');
}
res.end('Invalid token');
return;
}
const hashToken = Accounts._hashLoginToken(loginToken);
user = await ReactiveCache.getUser({
'services.resume.loginTokens.hashedToken': hashToken,
});
adminId = user._id.toString();
impersonateDone = await ReactiveCache.getImpersonatedUser({ adminId: adminId });
} else if (!Meteor.settings.public.sandstorm) {
try {
await Authentication.checkUserId(req.userId);
} catch (error) {
res.writeHead(error.statusCode || 403, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end('Unauthorized');
return;
}
user = await ReactiveCache.getUser({
_id: req.userId,
isAdmin: true,
});
}
let userLanguage = 'en';
if(user && user.profile){
userLanguage = user.profile.language
}
const exporterExcel = new ExporterExcel(boardId, userLanguage);
if ((await exporterExcel.canExport(user))) {
if (impersonateDone) {
await ImpersonatedUsers.insertAsync({
adminId: adminId,
boardId: boardId,
reason: 'exportExcel',
});
}
exporterExcel.build(res);
} else {
res.end(TAPi18n.__('user-can-not-export-excel'));
}
});
});