Fixed notifications config mapping and filename sanitation for cve/rce

add svg to login / index for favicon

ensure file sanitization before and during notification
This commit is contained in:
gitmotion
2025-03-13 13:23:08 -07:00
parent 81baf87e93
commit e11c9261f7
6 changed files with 67 additions and 36 deletions

View File

@@ -20,6 +20,9 @@ services:
- MAX_FILE_SIZE=1024 - MAX_FILE_SIZE=1024
- AUTO_UPLOAD=false - AUTO_UPLOAD=false
- DUMBDROP_TITLE=DumbDrop-Dev - DUMBDROP_TITLE=DumbDrop-Dev
# - APPRISE_URL=ntfy://dumbdrop-test
# - APPRISE_MESSAGE=[DEV] New file uploaded - {filename} ({size}), Storage used {storage}
# - APPRISE_SIZE_UNIT=auto
command: npm run dev command: npm run dev
restart: unless-stopped restart: unless-stopped
# Enable container debugging if needed # Enable container debugging if needed

View File

@@ -8,6 +8,7 @@
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/toastify-js/src/toastify.min.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/toastify-js/src/toastify.min.css">
<script src="https://cdn.jsdelivr.net/npm/toastify-js"></script> <script src="https://cdn.jsdelivr.net/npm/toastify-js"></script>
<link rel="manifest" href="/manifest.json"> <link rel="manifest" href="/manifest.json">
<link rel="icon" type="image/svg+xml" href="assets/icon.svg">
</head> </head>
<body> <body>
<div class="container"> <div class="container">

View File

@@ -5,6 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{SITE_TITLE}} - Login</title> <title>{{SITE_TITLE}} - Login</title>
<link rel="stylesheet" href="styles.css"> <link rel="stylesheet" href="styles.css">
<link rel="icon" type="image/svg+xml" href="assets/icon.svg">
<style> <style>
.login-container { .login-container {
display: flex; display: flex;

View File

@@ -10,7 +10,7 @@ const crypto = require('crypto');
const path = require('path'); const path = require('path');
const { config } = require('../config'); const { config } = require('../config');
const logger = require('../utils/logger'); const logger = require('../utils/logger');
const { getUniqueFilePath, getUniqueFolderPath } = require('../utils/fileUtils'); const { getUniqueFilePath, getUniqueFolderPath, sanitizeFilename } = require('../utils/fileUtils');
const { sendNotification } = require('../services/notifications'); const { sendNotification } = require('../services/notifications');
const fs = require('fs'); const fs = require('fs');
const { cleanupIncompleteUploads } = require('../utils/cleanup'); const { cleanupIncompleteUploads } = require('../utils/cleanup');
@@ -175,7 +175,8 @@ router.post('/init', async (req, res) => {
batchActivity.set(batchId, Date.now()); batchActivity.set(batchId, Date.now());
// Sanitize filename and convert to forward slashes // Sanitize filename and convert to forward slashes
const safeFilename = path.normalize(filename) const sanitizedFilename = sanitizeFilename(filename);
const safeFilename = path.normalize(sanitizedFilename)
.replace(/^(\.\.(\/|\\|$))+/, '') .replace(/^(\.\.(\/|\\|$))+/, '')
.replace(/\\/g, '/') .replace(/\\/g, '/')
.replace(/^\/+/, ''); // Remove leading slashes .replace(/^\/+/, ''); // Remove leading slashes
@@ -264,7 +265,7 @@ router.post('/init', async (req, res) => {
upload.writeStream.end(); upload.writeStream.end();
uploads.delete(uploadId); uploads.delete(uploadId);
logger.success(`Completed zero-byte file upload: ${upload.safeFilename}`); logger.success(`Completed zero-byte file upload: ${upload.safeFilename}`);
await sendNotification(upload.safeFilename, 0, config); sendNotification(upload.safeFilename, 0, config);
} }
// Send response // Send response
@@ -360,7 +361,7 @@ router.post('/chunk/:uploadId', express.raw({
} }
// Send notification // Send notification
await sendNotification(upload.safeFilename, upload.fileSize, config); sendNotification(upload.safeFilename, upload.fileSize, config);
logUploadState('After Upload Complete'); logUploadState('After Upload Complete');
} }

View File

@@ -4,13 +4,10 @@
* Handles message formatting and notification delivery. * Handles message formatting and notification delivery.
*/ */
const { exec } = require('child_process'); const { spawn } = require('child_process');
const util = require('util'); const { formatFileSize, calculateDirectorySize, sanitizeFilename } = require('../utils/fileUtils');
const { formatFileSize, calculateDirectorySize } = require('../utils/fileUtils');
const logger = require('../utils/logger'); const logger = require('../utils/logger');
const execAsync = util.promisify(exec);
/** /**
* Send a notification using Apprise * Send a notification using Apprise
* @param {string} filename - Name of uploaded file * @param {string} filename - Name of uploaded file
@@ -19,34 +16,56 @@ const execAsync = util.promisify(exec);
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async function sendNotification(filename, fileSize, config) { async function sendNotification(filename, fileSize, config) {
const { APPRISE_URL, APPRISE_MESSAGE, APPRISE_SIZE_UNIT, uploadDir } = config; const { appriseUrl, appriseMessage, appriseSizeUnit, uploadDir } = config;
if (!APPRISE_URL) { console.debug("NOTIFICATIONS CONFIG:", filename, fileSize, config);
return; if (!appriseUrl) {
} return;
}
try { try {
const formattedSize = formatFileSize(fileSize, APPRISE_SIZE_UNIT); const formattedSize = formatFileSize(fileSize, appriseSizeUnit);
const dirSize = await calculateDirectorySize(uploadDir); const dirSize = await calculateDirectorySize(uploadDir);
const totalStorage = formatFileSize(dirSize); const totalStorage = formatFileSize(dirSize);
// Sanitize the message components // Sanitize the filename to remove any special characters that could cause issues
const sanitizedFilename = JSON.stringify(filename).slice(1, -1); const sanitizedFilename = sanitizeFilename(filename); // apply sanitization of filename again (in case)
const message = APPRISE_MESSAGE
.replace('{filename}', sanitizedFilename)
.replace('{size}', formattedSize)
.replace('{storage}', totalStorage);
// Use string command for better escaping // Construct the notification message by replacing placeholders
const command = `apprise ${APPRISE_URL} -b "${message}"`; const message = appriseMessage
await execAsync(command, { shell: true }); .replace('{filename}', sanitizedFilename)
.replace('{size}', formattedSize)
.replace('{storage}', totalStorage);
logger.info(`Notification sent for: ${sanitizedFilename} (${formattedSize}, Total storage: ${totalStorage})`); await new Promise((resolve, reject) => {
} catch (err) { const appriseProcess = spawn('apprise', [appriseUrl, '-b', message]);
logger.error(`Failed to send notification: ${err.message}`);
} appriseProcess.stdout.on('data', (data) => {
logger.info(`Apprise Output: ${data.toString().trim()}`);
});
appriseProcess.stderr.on('data', (data) => {
logger.error(`Apprise Error: ${data.toString().trim()}`);
});
appriseProcess.on('close', (code) => {
if (code === 0) {
logger.info(`Notification sent for: ${sanitizedFilename} (${formattedSize}, Total storage: ${totalStorage})`);
resolve();
} else {
reject(new Error(`Apprise process exited with code ${code}`));
}
});
appriseProcess.on('error', (err) => {
reject(new Error(`Apprise process failed to start: ${err.message}`));
});
});
} catch (err) {
logger.error(`Failed to send notification: ${err.message}`);
}
} }
module.exports = { module.exports = {
sendNotification sendNotification,
}; };

View File

@@ -160,10 +160,16 @@ async function getUniqueFolderPath(folderPath) {
return finalPath; return finalPath;
} }
function sanitizeFilename(fileName) {
const sanitized = fileName.replace(/[<>:"/\\|?*]+/g, '').replace(/["`$|;&<>]/g, '');
return sanitized;
}
module.exports = { module.exports = {
formatFileSize, formatFileSize,
calculateDirectorySize, calculateDirectorySize,
ensureDirectoryExists, ensureDirectoryExists,
getUniqueFilePath, getUniqueFilePath,
getUniqueFolderPath getUniqueFolderPath,
sanitizeFilename
}; };