Files
BetterDesk/web-nodejs/middleware/csrf.js
T
UNITRONIX f3ad154e7a Force admin password update and CSRF fixes
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>
2026-03-05 22:42:14 +01:00

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
};