Files
wekan/models/lib/securityCategories.js
T
Lauri Ojansivu fdb683892d Log every filename/content sanitization to Admin Panel / Problems
Whenever a filename or file requires sanitization — on upload, migration, the
existing-file corrector, or viewing/serving — record it to the Security event
stream (action:'sanitized'), the companion to the existing action:'blocked'
upload rejections. Each row carries:

- WHEN: the event timestamp.
- WHO: the original uploader (userId), shown as a new clickable username column
  in the Problems event report.
- FOR WHAT REASON: sanitizationReasons() — URL-encoded name, invisible
  characters, typosquatting (look-alike characters), the specific exploit KIND
  (JavaScript code, XML code, XML loop (billion laughs), server-side PHP/ASP,
  template injection, HTML code), wrong file type (.txt -> .png), filename too
  long, or empty-name-generated.
- THE FILENAME: "from" -> "to".
- WHERE: board > swimlane > list > card, plus the board's organization(s) and
  team(s) (server/lib/fileContext.js).

New: classifyExploitKinds() + sanitizationReasons() (isomorphic), the
server-only fileContext resolver, filenameSanitizeLog helpers, catalog keys
file.sanitize / file.content / file.malware, and the uploader column in the
event report. Wired at all fix points (attachments/avatars onAfterUpload,
moveToStorage, correctFileExtensions, httpStreamOutput). EICAR now uses the
file.malware key. Logging is best-effort and never breaks the upload/view path.

Tests: classifyExploitKinds + sanitizationReasons unit tests (positive +
negative) and logging-wiring source guards. CHANGELOG + design doc updated.
2026-07-20 03:11:07 +03:00

47 lines
3.2 KiB
JavaScript

'use strict';
// Shared security category ↔ hall-of-fame *Bleed catalog
// (design: docs/Security/Remediation/WeKan.md §6).
//
// Maps a short guard key to { category (general name), bleed (hall-of-fame name),
// severity, cwe }. Guards pass a `key`; the logger fills the rest. Kept as one
// table so the general category and the *Bleed name never drift apart. CommonJS
// so both the server ESM code and the tests/*.test.cjs guards can load it.
const CATALOG = {
'ssrf.redirect': { category: 'ssrf', bleed: 'RedirectBleed', severity: 'high', cwe: 'CWE-918' },
'ssrf.attachment': { category: 'ssrf', bleed: 'LiveBleed', severity: 'high', cwe: 'CWE-918' },
'ssrf.fetch': { category: 'ssrf', bleed: 'DnsBleed', severity: 'high', cwe: 'CWE-918' },
'ssrf.webhook': { category: 'ssrf', bleed: 'IntegrationBleed', severity: 'high', cwe: 'CWE-918' },
'xss.source': { category: 'xss', bleed: 'SourceBleed', severity: 'high', cwe: 'CWE-79' },
'xss.mime': { category: 'xss', bleed: 'MimeBleed', severity: 'high', cwe: 'CWE-79' },
'xss.input': { category: 'xss', bleed: 'InputBleed', severity: 'medium', cwe: 'CWE-79' },
'spoofing.xff': { category: 'spoofing', bleed: 'MetricsBleed', severity: 'medium', cwe: 'CWE-290' },
'authz.export': { category: 'authz', bleed: 'ImpersonateBleed', severity: 'high', cwe: 'CWE-863' },
'authz.board': { category: 'authz', bleed: 'BoardBleed', severity: 'high', cwe: 'CWE-863' },
'auth-race.cas': { category: 'auth-race', bleed: 'CasBleed', severity: 'high', cwe: 'CWE-362' },
'auth-race.oidc': { category: 'auth-race', bleed: 'OidcBleed', severity: 'high', cwe: 'CWE-362' },
'brute.invite': { category: 'brute-force', bleed: 'InviteBleed', severity: 'high', cwe: 'CWE-307' },
'brute.login': { category: 'brute-force', bleed: 'BruteBleed', severity: 'medium', cwe: 'CWE-307' },
'injection.shell': { category: 'injection', bleed: 'ScannerBleed', severity: 'high', cwe: 'CWE-78' },
'file.mime': { category: 'file', bleed: 'MimeBleed', severity: 'high', cwe: 'CWE-434' },
'file.name': { category: 'file', bleed: 'FileBleed', severity: 'medium', cwe: 'CWE-73' },
'file.sanitize': { category: 'file', bleed: 'FileBleed', severity: 'info', cwe: 'CWE-73' },
'file.content': { category: 'file', bleed: 'FileBleed', severity: 'medium', cwe: 'CWE-79' },
'file.malware': { category: 'file', bleed: 'MalwareBleed', severity: 'high', cwe: 'CWE-509' },
'file.size': { category: 'file', bleed: 'SpaceBleed', severity: 'low', cwe: 'CWE-400' },
'file.disk': { category: 'file', bleed: 'FloppyBleed', severity: 'low', cwe: 'CWE-400' },
'file.avatar-url': { category: 'ssrf', bleed: 'RedirectBleed', severity: 'high', cwe: 'CWE-918' },
'file.policy': { category: 'file', bleed: 'PolicyBleed', severity: 'info', cwe: '' },
};
const DEFAULT = { category: 'unknown', bleed: 'Generic', severity: 'info', cwe: '' };
// Resolve a short guard key to its { category, bleed, severity, cwe }. Unknown
// keys fall back to a generic entry so the log/report is never blank.
function categoryFor(key) {
return Object.assign({}, DEFAULT, CATALOG[key] || {});
}
module.exports = { CATALOG, categoryFor };