mirror of
https://github.com/wekan/wekan.git
synced 2026-11-07 11:02:33 -05:00
- 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 !
109 lines
3.8 KiB
JavaScript
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'));
|
|
}
|
|
});
|
|
});
|