mirror of
https://github.com/UNITRONIX/BetterDesk.git
synced 2026-09-10 09:35:39 +00:00
8da6f8c9fd
Critical: - C-01: WebSocket Origin validation (CSWSH protection) via new middleware/wsOrigin.js, applied to wsRelay, chatRelay, remoteRelay, bdRelay, cdapTerminalProxy, cdapMediaProxy - C-02: Remove Tauri Origin-based CSRF bypass in server.js; only /api/bd/* skipped - C-03: Update vulnerable deps (express 4.21.2, multer 2.0.0, protobufjs 7.4.0, helmet 7.2.0, axios 1.9.0, cookie-parser 1.4.7, express-session 1.18.1) High: - H-01: Disable auto-create-local-user on login by default; opt-in via BETTERDESK_AUTH_AUTOCREATE=true (does not affect ensureDefaultAdmin fresh-install bootstrap) - H-02: Bearer-only auth for /api/bd/*; session cookie fallback removed; tokens redacted in logs - H-05: Replace custom PBKDF2 with golang.org/x/crypto/pbkdf2; 600k iterations; new format pbkdf2-sha256\\\ with backward-compat for legacy salt:hash Backward compatibility: legacy password hashes still verify; CSRF tokens still obtainable via csrfTokenProvider; fresh-install admin bootstrap (.admin_credentials flow) unchanged. Audit report: docs/security/AUDIT_PRODUCTION_2026-04-10.md This commit was made possible thanks to Insolve.
89 lines
3.4 KiB
JavaScript
89 lines
3.4 KiB
JavaScript
/**
|
|
* BetterDesk Console — WebSocket Origin validation
|
|
*
|
|
* Mitigates Cross-Site WebSocket Hijacking (CSWSH).
|
|
*
|
|
* Browsers do not enforce Same-Origin Policy on WebSocket upgrades — without
|
|
* explicit Origin validation, a malicious site can open a ws:// connection
|
|
* to the panel using the victim's session cookie.
|
|
*
|
|
* Allowed origins:
|
|
* 1. Always accept same-host upgrades (Origin scheme://host[:port] === request Host)
|
|
* 2. Optional whitelist via WS_ALLOWED_ORIGINS env (comma-separated, exact match)
|
|
* 3. Optional `BD_ALLOW_MISSING_ORIGIN=true` to allow non-browser clients
|
|
* that omit Origin (default: only allowed when there is no session cookie
|
|
* attached to the request — token-based clients should send Bearer).
|
|
*
|
|
* Returns true if the upgrade should proceed, false otherwise.
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const ALLOWED_FROM_ENV = (process.env.WS_ALLOWED_ORIGINS || '')
|
|
.split(',')
|
|
.map((s) => s.trim())
|
|
.filter(Boolean);
|
|
|
|
// Allow missing Origin only for clearly non-browser clients. Browsers always
|
|
// send Origin on WebSocket upgrades, so absent Origin + session cookie is
|
|
// suspicious. Opt-in only for niche reverse-proxy setups.
|
|
const ALLOW_MISSING_ORIGIN = process.env.BD_ALLOW_MISSING_ORIGIN === 'true';
|
|
|
|
function sameHostOrigin(request) {
|
|
const host = request.headers && request.headers.host;
|
|
if (!host) return null;
|
|
// Cannot know if the panel was served via HTTPS from inside upgrade handler,
|
|
// so accept both schemes for the request's own host. This is safe because
|
|
// an attacker cannot make the browser send a forged Host header.
|
|
return [`http://${host}`, `https://${host}`];
|
|
}
|
|
|
|
/**
|
|
* Validate WebSocket upgrade Origin.
|
|
* @param {http.IncomingMessage} request
|
|
* @returns {{ok: true} | {ok: false, reason: string}}
|
|
*/
|
|
function validateOrigin(request) {
|
|
const origin = (request.headers && request.headers.origin) || '';
|
|
const cookies = (request.headers && request.headers.cookie) || '';
|
|
const sameHost = sameHostOrigin(request);
|
|
|
|
if (!origin) {
|
|
// No Origin: browsers always send it. Allow only when there is no
|
|
// session cookie (token-based client) OR when explicitly opted-in.
|
|
const hasSessionCookie = /bd\.sid=|betterdesk\.sid=/.test(cookies);
|
|
if (!hasSessionCookie || ALLOW_MISSING_ORIGIN) {
|
|
return { ok: true };
|
|
}
|
|
return { ok: false, reason: 'missing Origin with session cookie present' };
|
|
}
|
|
|
|
if (sameHost && sameHost.includes(origin)) {
|
|
return { ok: true };
|
|
}
|
|
if (ALLOWED_FROM_ENV.includes(origin)) {
|
|
return { ok: true };
|
|
}
|
|
return { ok: false, reason: `Origin not in allow-list: ${origin}` };
|
|
}
|
|
|
|
/**
|
|
* Helper that rejects a WebSocket upgrade if the Origin is not allowed.
|
|
* Writes HTTP 403 and destroys the socket.
|
|
* @returns {boolean} true if accepted (caller should continue), false if rejected.
|
|
*/
|
|
function enforceOrigin(request, socket, label) {
|
|
const result = validateOrigin(request);
|
|
if (result.ok) return true;
|
|
const ip = (request.socket && request.socket.remoteAddress) || '?';
|
|
// eslint-disable-next-line no-console
|
|
console.warn(`[WS-ORIGIN] Rejected upgrade (${label || 'unknown'}) from ${ip}: ${result.reason}`);
|
|
try {
|
|
socket.write('HTTP/1.1 403 Forbidden\r\n\r\n');
|
|
socket.destroy();
|
|
} catch (_) { /* socket already closed */ }
|
|
return false;
|
|
}
|
|
|
|
module.exports = { validateOrigin, enforceOrigin, ALLOWED_FROM_ENV };
|