Files
LuHost/middleware/security.js
Nathan Schneider 3aed09b60f Initial commit: LuHost - Luanti Server Management Web Interface
A modern web interface for Luanti (Minetest) server management with ContentDB integration.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-23 17:32:37 -06:00

185 lines
4.7 KiB
JavaScript

// Security middleware for input validation and CSRF protection
/**
* Input validation middleware
* Validates common input patterns and sanitizes data
*/
function validateInput(req, res, next) {
// Sanitize query parameters
for (const key in req.query) {
if (typeof req.query[key] === 'string') {
// Remove control characters
req.query[key] = req.query[key].replace(/[\x00-\x1F\x7F]/g, '');
// Limit length
if (req.query[key].length > 1000) {
req.query[key] = req.query[key].substring(0, 1000);
}
}
}
// Sanitize body data for non-JSON requests
if (req.body && typeof req.body === 'object' && !Array.isArray(req.body)) {
for (const key in req.body) {
if (typeof req.body[key] === 'string') {
// Remove control characters but preserve newlines for textareas
if (key.includes('description') || key.includes('content') || key.includes('motd')) {
req.body[key] = req.body[key].replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
} else {
req.body[key] = req.body[key].replace(/[\x00-\x1F\x7F]/g, '');
}
// Limit length based on field type
const maxLength = getMaxLengthForField(key);
if (req.body[key].length > maxLength) {
req.body[key] = req.body[key].substring(0, maxLength);
}
}
}
}
next();
}
/**
* Get maximum allowed length for different field types
*/
function getMaxLengthForField(fieldName) {
const fieldLimits = {
// User authentication fields
'username': 50,
'password': 200,
'confirmPassword': 200,
'currentPassword': 200,
'newPassword': 200,
// Server/world names
'name': 100,
'worldName': 100,
'serverName': 200,
// Text content
'description': 2000,
'motd': 500,
'content': 5000,
// Commands and paths
'command': 500,
'path': 500,
// Network settings
'bind': 100,
'serverlist_url': 500,
// Default
'default': 200
};
return fieldLimits[fieldName] || fieldLimits['default'];
}
/**
* XSS protection middleware
* Escapes HTML in user input for specific fields
*/
function xssProtection(req, res, next) {
if (req.body && typeof req.body === 'object') {
// Fields that should be HTML escaped
const fieldsToEscape = ['username', 'name', 'worldName', 'serverName', 'motd'];
for (const field of fieldsToEscape) {
if (req.body[field] && typeof req.body[field] === 'string') {
req.body[field] = escapeHtml(req.body[field]);
}
}
}
next();
}
/**
* Basic HTML escape function
*/
function escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
/**
* Security headers middleware (additional to helmet)
*/
function additionalSecurityHeaders(req, res, next) {
// Prevent MIME type sniffing
res.setHeader('X-Content-Type-Options', 'nosniff');
// Prevent clickjacking
res.setHeader('X-Frame-Options', 'DENY');
// XSS protection
res.setHeader('X-XSS-Protection', '1; mode=block');
// Referrer policy
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
next();
}
/**
* Request size validation
*/
function validateRequestSize(req, res, next) {
// Check for unusually large requests that might indicate an attack
const contentLength = req.headers['content-length'];
if (contentLength && parseInt(contentLength) > 50 * 1024 * 1024) { // 50MB limit
return res.status(413).json({ error: 'Request too large' });
}
next();
}
/**
* Path traversal protection
*/
function pathTraversalProtection(req, res, next) {
// Check for path traversal attempts in various parameters
const suspiciousPatterns = ['../', '..\\', '%2e%2e%2f', '%2e%2e%5c'];
function checkForTraversal(obj, path = '') {
for (const [key, value] of Object.entries(obj)) {
if (typeof value === 'string') {
const lowerValue = value.toLowerCase();
for (const pattern of suspiciousPatterns) {
if (lowerValue.includes(pattern)) {
console.warn(`Path traversal attempt detected: ${path}${key} = ${value}`);
return true;
}
}
} else if (typeof value === 'object' && value !== null) {
if (checkForTraversal(value, `${path}${key}.`)) {
return true;
}
}
}
return false;
}
if ((req.query && checkForTraversal(req.query)) ||
(req.body && checkForTraversal(req.body))) {
return res.status(400).json({ error: 'Invalid request parameters' });
}
next();
}
module.exports = {
validateInput,
xssProtection,
additionalSecurityHeaders,
validateRequestSize,
pathTraversalProtection,
escapeHtml
};