Files
wekan/server/lib/secureTransfer.js
Lauri Ojansivu 567f99227b A failed import or export disables an account only when it was an attempt.
secureTransfer recorded every throw as a high "blocked" event naming the
user, and that disables the account. sanitize-html 2.18.0 threw
RangeError on nested <iframe>, so a card planted with the payload locked
out the next member who exported the board; any library fault would do
the same. The transfer boundary now marks its own refusals
(TransferBoundaryError: a cycle, a non-plain object, a size limit), and
models/lib/transferFailure.cjs records only those as blocked - high, and
medium for a size limit as before. Anything else is ImportExportFailure:
detected, still naming whose import or export it was, and never blocking
an account.

tests/transferFailure.test.cjs drives real boundary refusals and a
sanitizer that throws through blockOnSecurityEvent's own decision; with
the old secureTransfer it fails. It also fails if the boundary throws a
plain Error or anything but secureTransfer calls it.

Thanks to xet7 !
2026-10-22 04:56:40 +03:00

45 lines
1.8 KiB
JavaScript

import { sanitizeTransferValue } from '/models/lib/importExportBoundary';
import { sanitizeInput } from '/server/lib/inputSanitizer';
// Server wrapper around the pure structural boundary. This is deliberately the
// only place that couples transfer validation to server HTML sanitization and Admin Problems,
// so every adapter records blocked and sanitized attempts consistently.
export function secureTransfer(value, context = {}) {
try {
const result = sanitizeTransferValue(value, {
direction: context.direction || 'import',
sanitizeHtml: sanitizeInput,
maxDepth: context.maxDepth,
maxNodes: context.maxNodes,
maxArray: context.maxArray,
maxString: context.maxString,
maxBinaryString: context.maxBinaryString,
});
if (result.warnings.length) {
require('/server/lib/securityLog').record({
category: 'Import/export validation',
bleed: 'ImportExportSanitization',
severity: 'warning',
action: 'sanitized',
source: context.source || `${context.direction || 'import'}:unknown`,
userId: context.userId,
username: context.username,
ip: context.ip,
detail: `${result.warnings.length} unsafe value(s): ${result.warnings.slice(0, 5).map(w => `${w.path} ${w.reason}`).join('; ')}`,
});
}
return result.value;
} catch (error) {
// Only the boundary's own refusals are the user's attempt; a fault of the
// code (a sanitizer that throws) is recorded without blocking anybody.
// See models/lib/transferFailure.cjs.
try {
const { transferFailureEvent } = require('/models/lib/transferFailure.cjs');
require('/server/lib/securityLog').record(transferFailureEvent(error, context));
} catch (e) { /* logging must never break the guard */ }
throw error;
}
}
export default secureTransfer;