mirror of
https://github.com/wekan/wekan.git
synced 2026-11-07 02:52: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 !
134 lines
4.8 KiB
JavaScript
134 lines
4.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() {
|
||
const { ExporterExcelCard, ALL_FIELDS } = require('./server/ExporterExcelCard');
|
||
const { WebApp } = require('meteor/webapp');
|
||
const { Authentication } = require('/server/authentication');
|
||
|
||
/**
|
||
* @operation exportExcelCard
|
||
* @tag Cards
|
||
*
|
||
* @summary Export a single card to Excel (.xlsx), formatted for DIN A4 Portrait printing.
|
||
*
|
||
* @description If user is already logged-in, pass loginToken as param
|
||
* "authToken": '/api/boards/:boardId/lists/:listId/cards/:cardId/exportExcel?authToken=:token'
|
||
*
|
||
* Optional query param "fields" is a comma-separated list of sections to include.
|
||
* Valid values: people, board-info, dates, description, checklists, subtasks, comments
|
||
* Omitting "fields" includes all sections.
|
||
*
|
||
* @param {string} boardId the ID of the board
|
||
* @param {string} listId the ID of the list
|
||
* @param {string} cardId the ID of the card to export
|
||
* @param {string} authToken the loginToken
|
||
* @param {string} fields comma-separated list of sections to include
|
||
*/
|
||
WebApp.handlers.get(
|
||
'/api/boards/:boardId/lists/:listId/cards/:cardId/exportExcel',
|
||
async function (req, res) {
|
||
const boardId = req.params.boardId;
|
||
const paramListId = req.params.listId;
|
||
const paramCardId = req.params.cardId;
|
||
let user = null;
|
||
let impersonateDone = false;
|
||
let adminId = null;
|
||
|
||
const board = await ReactiveCache.getBoard(boardId);
|
||
if (!board) {
|
||
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
|
||
res.end('Board not found');
|
||
return;
|
||
}
|
||
|
||
// Public boards skip authentication
|
||
if (board.isPublic()) {
|
||
const fieldsParam = req.query.fields;
|
||
const fields = fieldsParam
|
||
? fieldsParam.split(',').map(f => f.trim()).filter(f => ALL_FIELDS.includes(f))
|
||
: null;
|
||
const exporter = new ExporterExcelCard(boardId, paramListId, paramCardId, 'en', fields);
|
||
await exporter.build(res);
|
||
return;
|
||
}
|
||
|
||
// Authenticate for private boards
|
||
const loginToken = req.query.authToken;
|
||
if (loginToken) {
|
||
if (loginToken.length > 10000) {
|
||
res.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' });
|
||
res.end('Invalid token');
|
||
return;
|
||
}
|
||
|
||
const hashToken = Accounts._hashLoginToken(loginToken);
|
||
user = await ReactiveCache.getUser({
|
||
'services.resume.loginTokens.hashedToken': hashToken,
|
||
});
|
||
if (!user) {
|
||
res.writeHead(401, { 'Content-Type': 'text/plain; charset=utf-8' });
|
||
res.end('Invalid token');
|
||
return;
|
||
}
|
||
adminId = user._id.toString();
|
||
impersonateDone = await ReactiveCache.getImpersonatedUser({ 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,
|
||
});
|
||
}
|
||
|
||
// Determine language: prefer the active UI language sent by the client
|
||
// (?lang=fi), then the profile setting, then English. The client-side
|
||
// param is necessary because WeKan may use the browser locale without
|
||
// ever writing it to profile.language.
|
||
let userLanguage =
|
||
req.query.lang ||
|
||
(user && user.profile && user.profile.language) ||
|
||
'en';
|
||
|
||
// Ensure the chosen language bundle is loaded into i18next.
|
||
try {
|
||
await TAPi18n.loadLanguage(userLanguage);
|
||
} catch (_) {
|
||
// Unknown / unsupported language – fall back to English silently.
|
||
userLanguage = 'en';
|
||
}
|
||
|
||
const dateFormat = (user && user.profile && user.profile.dateFormat) || 'YYYY-MM-DD';
|
||
|
||
// Parse optional ?fields=people,dates,... query param
|
||
const fieldsParam = req.query.fields;
|
||
const fields = fieldsParam
|
||
? fieldsParam.split(',').map(f => f.trim()).filter(f => ALL_FIELDS.includes(f))
|
||
: null;
|
||
|
||
const exporter = new ExporterExcelCard(boardId, paramListId, paramCardId, userLanguage, fields, dateFormat);
|
||
if ((await exporter.canExport(user))) {
|
||
if (impersonateDone) {
|
||
await ImpersonatedUsers.insertAsync({
|
||
adminId,
|
||
boardId,
|
||
reason: 'exportExcelCard',
|
||
});
|
||
}
|
||
await exporter.build(res);
|
||
} else {
|
||
res.end(TAPi18n.__('user-can-not-export-card-to-excel'));
|
||
}
|
||
},
|
||
);
|
||
});
|