- Pfad-Traversal: safeFilename/containedPath-Helper, storeAnhang sanitizes filename, alle Download/Delete/Mail-Send-Routen pruefen Containment - Stored XSS: serveInline entscheidet Viewable-Typ nur nach Extension, nicht nach client/seitigem MIME; nicht viewbare Typen werden als Download erzwungen. Upload fileFilter (Basis/Interne) + Extension-Validierung (Signatur/Foto leiten Ext aus MIME, blockieren .html) - URL-Scheme-Allowlist (safeUrl) fuer quelle_url-hrefs gegen javascript:-XSS - E-Mail-Iframe: Sandbox auf allow-same-only (kein allow-popups-to-escape) - Sicherheits-Header: CSP, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, COOP; x-powered-by aus; jsPDF self-hosted unter /vendor - Session: Secure-Flag bei TLS, serverseitige absoluteexpiry, Scrypt async + Dummy-Verify gegen Timing/Enumerate + Login-Rate-Limit - Open Redirect: /email/fetch nur same-origin Redirects - SSRF: Validierung von OLLAMA_HOST/CALDAV_URL/MAIL_HOST gegen Metadata/Link-Local-BLock (localhost/LAN bleibt erlaubt) - Globaler Error-Handler ohne Interna-Leak, env=production Co-Authored-By: Claude <noreply@anthropic.com>
43 lines
1.9 KiB
JavaScript
43 lines
1.9 KiB
JavaScript
// Password hashing using Node's built-in scrypt + a per-hash random salt.
|
|
//
|
|
// No external dependency (bcrypt would need a native build step). scrypt is
|
|
// memory-hard and well suited for interactive logins. Hash format:
|
|
// "<saltHex>:<hashHex>" (salt is 16 bytes, hash is 64 bytes)
|
|
//
|
|
// verify() is async (crypto.scrypt, non-blocking) so a login attempt cannot pin
|
|
// the event loop and stall every other request — a synchronous scrypt was a
|
|
// cheap CPU-DoS vector. DUMMY_HASH is a real hash for the "unknown user" path so
|
|
// login timing does not leak which usernames exist.
|
|
|
|
const crypto = require('crypto');
|
|
|
|
const KEYLEN = 64;
|
|
const DUMMY_SALT = '00000000000000000000000000000000';
|
|
const DUMMY_HASH = `${DUMMY_SALT}:${crypto.scryptSync('nextjobs-dummy', DUMMY_SALT, KEYLEN).toString('hex')}`;
|
|
|
|
function hash(password) {
|
|
const salt = crypto.randomBytes(16).toString('hex');
|
|
const out = crypto.scryptSync(password, salt, KEYLEN).toString('hex');
|
|
return `${salt}:${out}`;
|
|
}
|
|
|
|
// Resolve to true/false. Always runs a full scrypt (constant work) before
|
|
// deciding, so the caller cannot tell a wrong password from a bad hash format.
|
|
function verify(password, stored) {
|
|
return new Promise((resolve) => {
|
|
if (typeof stored !== 'string' || !stored.includes(':')) return resolve(false);
|
|
const idx = stored.indexOf(':');
|
|
const salt = stored.slice(0, idx);
|
|
const expected = stored.slice(idx + 1);
|
|
if (!salt || !expected) return resolve(false);
|
|
crypto.scrypt(String(password == null ? '' : password), salt, KEYLEN, (err, buf) => {
|
|
if (err) return resolve(false);
|
|
const computed = buf.toString('hex');
|
|
if (computed.length !== expected.length) return resolve(false);
|
|
// Constant-time compare to avoid timing side channels.
|
|
resolve(crypto.timingSafeEqual(Buffer.from(computed), Buffer.from(expected)));
|
|
});
|
|
});
|
|
}
|
|
|
|
module.exports = { hash, verify, DUMMY_HASH }; |