Files
BetterDesk/web-nodejs/middleware/security.js
T
UNITRONIX 89e4e0a592 Refactor routes, relay, auth & chat integrations
Several cross-cutting fixes and refactors:

- API routing: Consolidated multiple org policy PUT endpoints into a single parameterized route (/api/org/{id}/policy/{category}) to simplify handlers (api/server.go).
- Relay/LAN handling: Prefer server LAN IP for LAN relay addresses (with proper port detection from configured relay), added strconv import and fallback to configured relay when LAN IP unknown; improves reliability with NAT hairpin issues (signal/handler.go).
- Web security: Allow inline event handlers (scriptSrcAttr: 'unsafe-inline') while still requiring nonced <script> tags for scripts used by admin pages (web-nodejs/middleware/security.js).
- CSRF: Include x-csrf-token header when POSTing language fix requests from the UI (web-nodejs/public/js/languages.js).
- Admin roles: Broaden admin checks to accept multiple admin role names via ADMIN_ROLES and update requireAdmin and RustDesk payload is_admin logic (web-nodejs/routes/rustdesk-api.routes.js).
- Admin credentials & startup resilience: authService now searches additional candidate dirs (config.dataDir, extra Windows/RustDesk paths, /app/data in Docker), skips empty candidates, and increases retries/delays when waiting for .admin_credentials (web-nodejs/services/authService.js).
- Chat API paths: Updated chat-related API calls to use /chat/* (instead of /api/chat/*) across persistence, history, contacts, groups, read and other operations (web-nodejs/services/chatRelay.js).

These changes improve compatibility (LAN relay, admin detection), robustness (longer retries, Docker/Windows path checks), and frontend behavior (CSRF token, inline handlers, updated chat endpoints).
2026-04-12 19:28:16 +02:00

105 lines
3.7 KiB
JavaScript

/**
* BetterDesk Console - Security Middleware
* Configures Helmet and custom security headers
*/
const crypto = require('crypto');
const helmet = require('helmet');
const config = require('../config/config');
/**
* Build CSP connect-src based on HTTPS mode
* Allow WebSocket connections (ws:// or wss:// depending on mode)
*/
const connectSources = config.httpsEnabled
? ["'self'", "wss:"]
: ["'self'", "ws:"];
function buildHelmetMiddleware(req, res) {
const nonce = crypto.randomBytes(16).toString('base64');
const isRemoteViewerPage = req.path.startsWith('/remote');
res.locals.cspNonce = nonce;
const scriptSources = ["'self'", `'nonce-${nonce}'`];
if (isRemoteViewerPage) {
// The remote viewer still depends on protobuf.js runtime code generation.
scriptSources.push("'unsafe-eval'");
}
return helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: scriptSources,
// Allow inline event handlers (onclick=, onchange=, etc.) used by
// several admin panel pages. <script> tags still require nonce.
scriptSrcAttr: ["'unsafe-inline'"],
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
fontSrc: ["'self'", "https://fonts.gstatic.com"],
imgSrc: ["'self'", "data:", "blob:"],
mediaSrc: ["'self'", "blob:"],
connectSrc: connectSources,
frameSrc: ["'self'"],
objectSrc: ["'none'"],
childSrc: ["'self'"],
workerSrc: ["'self'", "blob:"],
baseUri: ["'self'"],
formAction: ["'self'"],
frameAncestors: ["'self'"],
upgradeInsecureRequests: config.httpsEnabled ? [] : null
}
},
crossOriginEmbedderPolicy: false,
crossOriginResourcePolicy: { policy: 'same-origin' },
crossOriginOpenerPolicy: config.httpsEnabled ? { policy: 'same-origin' } : false,
originAgentCluster: config.httpsEnabled,
strictTransportSecurity: config.httpsEnabled
? { maxAge: 31536000, includeSubDomains: true, preload: false }
: false,
dnsPrefetchControl: { allow: false },
referrerPolicy: { policy: 'strict-origin-when-cross-origin' }
});
}
/**
* Custom security headers beyond what Helmet provides
*/
function customSecurityHeaders(req, res, next) {
// Prevent clickjacking (belt + suspenders with CSP frame-ancestors)
res.setHeader('X-Frame-Options', 'SAMEORIGIN');
// Prevent MIME type sniffing
res.setHeader('X-Content-Type-Options', 'nosniff');
// XSS Protection (disabled — CSP is the modern replacement)
res.setHeader('X-XSS-Protection', '0');
// Permissions policy — restrict powerful APIs
res.setHeader('Permissions-Policy',
'geolocation=(), microphone=(self), camera=(), ' +
'browsing-topics=(), payment=(), usb=(), ' +
'accelerometer=(), gyroscope=(), magnetometer=()');
// Prevent cross-site leak via cache timing
res.setHeader('Cache-Control', 'no-store');
// Allow static assets to be cached (overridden in express.static options)
if (req.path.startsWith('/css/') || req.path.startsWith('/js/') ||
req.path.startsWith('/img/') || req.path.startsWith('/fonts/')) {
res.setHeader('Cache-Control', 'public, max-age=3600');
}
next();
}
/**
* Combined security middleware
*/
function securityMiddleware(req, res, next) {
buildHelmetMiddleware(req, res)(req, res, () => {
customSecurityHeaders(req, res, next);
});
}
module.exports = securityMiddleware;