Files
dashy/services/save-config.js
Alicia Sykes 21491bbe22 ️ Robustness improvments for save-config endpoint
Deeper path validation, collision protection for backups, fixes missing
config field crash, adds check for file size, improved security of
filename allow regex
2026-04-11 20:35:02 +01:00

74 lines
2.6 KiB
JavaScript

/**
* This file exports a function, used by the write config endpoint.
* It will make a backup of the users conf.yml file
* and then write their new config into the main conf.yml file.
* Finally, it will call a function with the status message
*/
const fsPromises = require('fs').promises;
const path = require('path');
const MAX_CONFIG_BYTES = 256 * 1024;
// Disallow paths having path separators, control chars (NUL/CR/LF), or ..
const SAFE_FILENAME = /^(?!\.+$)[^\\/\0\r\n]+\.ya?ml$/i;
module.exports = async (newConfig, render) => {
const respond = (success, message) => render(JSON.stringify({ success, message }));
// Validate request body
if (!newConfig || typeof newConfig.config !== 'string' || newConfig.config.length === 0) {
respond(false, "Request body is missing or has an invalid 'config' field");
return;
}
if (newConfig.config.length > MAX_CONFIG_BYTES) {
respond(false, `Config exceeds maximum size of ${MAX_CONFIG_BYTES / 1024} KB`);
return;
}
// If `filename` (for sub-pages) is specified validate and set it
let usersFileName;
if (typeof newConfig.filename === 'string' && newConfig.filename) {
const base = path.basename(newConfig.filename);
if (!SAFE_FILENAME.test(base)) {
respond(false, 'Invalid filename: must be a basename ending in .yml or .yaml');
return;
}
usersFileName = base;
}
// Resolve paths
const userDataDirectory = process.env.USER_DATA_DIR || './user-data/';
const backupLocation = process.env.BACKUP_DIR || path.join(userDataDirectory, 'config-backups');
const targetFile = usersFileName || 'conf.yml';
const targetFilePath = path.join(userDataDirectory, targetFile);
const backupBase = targetFile.replace(/\.ya?ml$/i, '');
const backupFilePath = path.join(backupLocation, `${backupBase}-${Date.now()}.backup.yml`);
// Backup current config before proceeding
try {
await fsPromises.mkdir(backupLocation, { recursive: true });
await fsPromises.copyFile(targetFilePath, backupFilePath);
} catch (error) {
if (error.code !== 'ENOENT') {
respond(false, `Unable to backup ${targetFile}: ${error}`);
return;
}
}
// Write the new config
try {
await fsPromises.writeFile(targetFilePath, newConfig.config, { encoding: 'utf8' });
} catch (error) {
respond(false, `Unable to write to ${targetFile}: ${error}`);
return;
}
// If successful, then render hasn't yet been called- call it
respond(
true,
`Successfully backed up ${targetFile} to ${backupFilePath}, `
+ `and updated the contents of ${targetFilePath}`,
);
};