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>
185 lines
4.7 KiB
JavaScript
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, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'");
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
}; |