mirror of
https://github.com/UNITRONIX/BetterDesk.git
synced 2026-09-10 17:45:42 +00:00
f3ad154e7a
Ensure freshly generated admin passwords are actually applied on reinstall and make CSRF handling more robust. Install scripts (Docker, POSH, sh) now remove stale auth.db from the console_data/data volume or data dir and create a .force_password_update sentinel (Docker also sets FORCE_PASSWORD_UPDATE=true) so the Node.js console will force-update the admin password. web-nodejs/services/authService.js adds checkForcePasswordUpdate() (env var or sentinel) and will forcibly update the admin bcrypt hash when requested; sentinel files are removed after detection. CSRF middleware now only generates tokens for safe methods (GET/HEAD/OPTIONS), avoids interfering with state-changing requests, and clears corrupt cookies using consistent options (httpOnly, sameSite: 'lax', secure based on config) to prevent spurious 403s. server.js mounts rustdeskApiRoutes before CSRF so bearer-token desktop clients are not blocked by cookie-based CSRF. Also unified the health check path from /api/health to /health in diagnostics checks and added required fs/path imports. Co-Authored-By: MrBrodacz - Design <215021251+MrBrodacz2025@users.noreply.github.com> Co-Authored-By: boruto79 <176351662+boruto79@users.noreply.github.com>
111 lines
3.9 KiB
JavaScript
111 lines
3.9 KiB
JavaScript
/**
|
|
* BetterDesk Console - CSRF Protection Middleware
|
|
* Uses csrf-csrf (double-submit cookie pattern) for stateless CSRF protection.
|
|
*
|
|
* Token flow:
|
|
* 1. Server generates token, sets it as a cookie + passes to EJS views
|
|
* 2. Client JS reads window.BetterDesk.csrfToken and sends it in X-CSRF-Token header
|
|
* 3. Middleware validates header matches cookie on state-changing requests (POST/PUT/DELETE/PATCH)
|
|
*/
|
|
|
|
const { doubleCsrf } = require('csrf-csrf');
|
|
const config = require('../config/config');
|
|
|
|
const {
|
|
generateToken,
|
|
doubleCsrfProtection
|
|
} = doubleCsrf({
|
|
getSecret: () => config.sessionSecret,
|
|
cookieName: '__csrf',
|
|
cookieOptions: {
|
|
httpOnly: true,
|
|
sameSite: 'lax',
|
|
secure: config.httpsEnabled,
|
|
path: '/'
|
|
},
|
|
getTokenFromRequest: (req) => {
|
|
// Read token from X-CSRF-Token header (set by public/js/utils.js)
|
|
return req.headers['x-csrf-token'] || req.body?._csrf || '';
|
|
}
|
|
});
|
|
|
|
/** Safe HTTP methods that render views and need a fresh CSRF token. */
|
|
const SAFE_METHODS = new Set(['GET', 'HEAD', 'OPTIONS']);
|
|
|
|
/**
|
|
* Middleware that generates a CSRF token and makes it available to views.
|
|
* Must be applied AFTER cookie-parser and session middleware.
|
|
*
|
|
* IMPORTANT: Token generation runs ONLY on safe methods (GET/HEAD/OPTIONS).
|
|
* On state-changing methods (POST/PUT/DELETE/PATCH) we skip generation
|
|
* entirely so that we never interfere with doubleCsrfProtection's
|
|
* validation of req.cookies — calling generateToken on POST with an
|
|
* invalid cookie would delete req.cookies['__csrf'] in the catch block
|
|
* before the validation middleware could read it, causing spurious 403s.
|
|
*
|
|
* If the existing __csrf cookie is malformed (e.g. leftover from an older
|
|
* installation with a different secret), we clear it and regenerate so
|
|
* the next page load works.
|
|
*/
|
|
function csrfTokenProvider(req, res, next) {
|
|
// State-changing methods: let doubleCsrfProtection handle everything
|
|
if (!SAFE_METHODS.has(req.method)) {
|
|
return next();
|
|
}
|
|
|
|
try {
|
|
const token = generateToken(req, res);
|
|
res.locals.csrfToken = token;
|
|
return next();
|
|
} catch (_err) {
|
|
// Cookie exists but is invalid (e.g. secret changed after reinstall).
|
|
// Clear the corrupt cookie so the browser forgets it, then generate
|
|
// a fresh token+cookie pair.
|
|
if (req.cookies) delete req.cookies['__csrf'];
|
|
res.clearCookie('__csrf', {
|
|
httpOnly: true,
|
|
sameSite: 'lax',
|
|
secure: config.httpsEnabled,
|
|
path: '/'
|
|
});
|
|
try {
|
|
const token = generateToken(req, res);
|
|
res.locals.csrfToken = token;
|
|
} catch (_e) {
|
|
// Give views a harmless empty token so rendering never breaks
|
|
res.locals.csrfToken = '';
|
|
}
|
|
return next();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Wrapper around doubleCsrfProtection that tolerates corrupt cookies on
|
|
* safe HTTP methods (GET / HEAD / OPTIONS). On those methods the library
|
|
* should never block — but a malformed __csrf cookie can still make it
|
|
* throw. We catch that, wipe the cookie and let the request through.
|
|
*/
|
|
function safeCsrfProtection(req, res, next) {
|
|
doubleCsrfProtection(req, res, (err) => {
|
|
if (err && SAFE_METHODS.has(req.method)) {
|
|
// Corrupt cookie on a safe method — clear and continue.
|
|
// clearCookie must use identical options for the browser to match.
|
|
res.clearCookie('__csrf', {
|
|
httpOnly: true,
|
|
sameSite: 'lax',
|
|
secure: config.httpsEnabled,
|
|
path: '/'
|
|
});
|
|
if (req.cookies) delete req.cookies['__csrf'];
|
|
return next();
|
|
}
|
|
// For state-changing methods (POST/PUT/DELETE/PATCH) propagate normally
|
|
return next(err);
|
|
});
|
|
}
|
|
|
|
module.exports = {
|
|
csrfTokenProvider,
|
|
doubleCsrfProtection: safeCsrfProtection
|
|
};
|