From 1727e64186f1f811b0cf5b18884a599f14c92562 Mon Sep 17 00:00:00 2001 From: Miroslav Pejic Date: Fri, 31 Jan 2025 12:56:07 +0100 Subject: [PATCH] [mirotalksfu] - improve security, update dep --- app/src/HtmlInjector.js | 12 ++++---- app/src/Server.js | 57 +++++++++++++++++++++++++++++++++----- app/src/config.template.js | 1 + cloud/package.json | 6 ++-- cloud/server.js | 35 +++++++++++++++++++++-- package.json | 6 ++-- public/js/Brand.js | 2 +- public/js/Room.js | 16 ++++------- public/js/RoomClient.js | 2 +- 9 files changed, 105 insertions(+), 32 deletions(-) diff --git a/app/src/HtmlInjector.js b/app/src/HtmlInjector.js index 06e38d19..1fab646f 100644 --- a/app/src/HtmlInjector.js +++ b/app/src/HtmlInjector.js @@ -18,14 +18,14 @@ class HtmlInjector { // Function to get dynamic data for injection (e.g., OG data, title, etc.) getInjectData() { return { - OG_TYPE: this.config.og?.type || 'app-webrtc', - OG_SITE_NAME: this.config.og?.siteName || 'MiroTalk SFU', - OG_TITLE: this.config.og?.title || 'Click the link to make a call.', + OG_TYPE: this.config?.og?.type || 'app-webrtc', + OG_SITE_NAME: this.config?.og?.siteName || 'MiroTalk SFU', + OG_TITLE: this.config?.og?.title || 'Click the link to make a call.', OG_DESCRIPTION: - this.config.og?.description || + this.config?.og?.description || 'MiroTalk SFU calling provides real-time video calls, messaging and screen sharing.', - OG_IMAGE: this.config.og?.image || 'https://sfu.mirotalk.com/images/mirotalksfu.png', - OG_URL: this.config.og?.url || 'https://sfu.mirotalk.com', + OG_IMAGE: this.config?.og?.image || 'https://sfu.mirotalk.com/images/mirotalksfu.png', + OG_URL: this.config?.og?.url || 'https://sfu.mirotalk.com', // Add more data here as needed with fallbacks }; } diff --git a/app/src/Server.js b/app/src/Server.js index 1bfa376f..4d7f3bc8 100644 --- a/app/src/Server.js +++ b/app/src/Server.js @@ -55,7 +55,7 @@ dev dependencies: { * @license For commercial or closed source, contact us at license.mirotalk@gmail.com or purchase directly via CodeCanyon * @license CodeCanyon: https://codecanyon.net/item/mirotalk-sfu-webrtc-realtime-video-conferences/40769970 * @author Miroslav Pejic - miroslav.pejic.85@gmail.com - * @version 1.7.19 + * @version 1.7.20 * */ @@ -73,8 +73,10 @@ const axios = require('axios'); const ngrok = require('ngrok'); const jwt = require('jsonwebtoken'); const fs = require('fs'); +const sanitizeFilename = require('sanitize-filename'); +const helmet = require('helmet'); const config = require('./config'); -const checkXSS = require('./XSS.js'); +const checkXSS = require('./XSS'); const Host = require('./Host'); const Room = require('./Room'); const Peer = require('./Peer'); @@ -226,16 +228,28 @@ const OIDC = config.oidc ? config.oidc : { enabled: false }; const dir = { public: path.join(__dirname, '../../', 'public'), rec: path.join(__dirname, '../', config?.server?.recording?.dir ? config.server.recording.dir + '/' : 'rec/'), + rtmp: path.join(__dirname, '../', config?.rtmp?.dir ? config.rtmp.dir + '/' : 'rtmp/'), }; -// rec directory create +// Rec directory create and set max file size +const recMaxFileSize = config?.server?.recording?.maxFileSize || 1 * 1024 * 1024 * 1024; // 1GB default const serverRecordingEnabled = config?.server?.recording?.enabled; if (serverRecordingEnabled) { + log.debug('Server Recording enabled creating dir', dir.rtmp); if (!fs.existsSync(dir.rec)) { fs.mkdirSync(dir.rec, { recursive: true }); } } +// Rtmp directory create +const rtmpEnabled = rtmpCfg && rtmpCfg.enabled; +if (rtmpEnabled) { + log.debug('RTMP enabled creating dir', dir.rtmp); + if (!fs.existsSync(dir.rtmp)) { + fs.mkdirSync(dir.rtmp, { recursive: true }); + } +} + // html views const views = { html: path.join(__dirname, '../../public/views'), @@ -340,6 +354,8 @@ function OIDCAuth(req, res, next) { function startServer() { // Start the app + app.use(helmet.xssFilter()); // Enable XSS protection + app.use(helmet.noSniff()); // Enable content type sniffing prevention app.use(express.static(dir.public)); app.use(cors(corsOptions)); app.use(compression()); @@ -769,8 +785,10 @@ function startServer() { return res.status(400).send('Filename not provided'); } - if (!Validator.isValidRecFileNameFormat(fileName)) { - log.warn('[RecSync] - Invalid file name', fileName); + // Sanitize and validate filename + const safeFileName = sanitizeFilename(fileName); + if (safeFileName !== fileName || !Validator.isValidRecFileNameFormat(fileName)) { + log.warn('[RecSync] - Invalid file name:', fileName); return res.status(400).send('Invalid file name'); } @@ -782,11 +800,37 @@ function startServer() { return res.status(400).send('Invalid file name'); } + // Ensure directory exists if (!fs.existsSync(dir.rec)) { fs.mkdirSync(dir.rec, { recursive: true }); } - const filePath = dir.rec + fileName; + + // Resolve and validate file path + const filePath = path.resolve(dir.rec, fileName); + if (!filePath.startsWith(path.resolve(dir.rec))) { + log.warn('[RecSync] - Attempt to save file outside allowed directory:', fileName); + return res.status(400).send('Invalid file path'); + } + + //Validate content type + if (!['application/octet-stream'].includes(req.headers['content-type'])) { + log.warn('[RecSync] - Invalid content type:', req.headers['content-type']); + return res.status(400).send('Invalid content type'); + } + + // Set up write stream and handle file upload const writeStream = fs.createWriteStream(filePath, { flags: 'a' }); + let receivedBytes = 0; + + req.on('data', (chunk) => { + receivedBytes += chunk.length; + if (receivedBytes > recMaxFileSize) { + req.destroy(); // Stop receiving data + writeStream.destroy(); // Stop writing data + log.warn('[RecSync] - File size exceeds limit:', fileName); + return res.status(413).send('File too large'); + } + }); req.pipe(writeStream); @@ -841,7 +885,6 @@ function startServer() { }); app.get('/rtmpEnabled', (req, res) => { - const rtmpEnabled = rtmpCfg && rtmpCfg.enabled; log.debug('RTMP enabled', rtmpEnabled); res.json({ enabled: rtmpEnabled }); }); diff --git a/app/src/config.template.js b/app/src/config.template.js index 973d1eb1..fe03ed1f 100644 --- a/app/src/config.template.js +++ b/app/src/config.template.js @@ -104,6 +104,7 @@ module.exports = { enabled: false, endpoint: '', // Change the URL if you want to save the recording to a different server or cloud service (http://localhost:8080), otherwise leave it as is (empty). dir: 'rec', + maxFileSize: 1 * 1024 * 1024 * 1024, // 1 GB }, rtmp: { /* diff --git a/cloud/package.json b/cloud/package.json index 44bf524f..c776a8ff 100644 --- a/cloud/package.json +++ b/cloud/package.json @@ -15,9 +15,11 @@ "license": "AGPLv3", "dependencies": { "cors": "2.8.5", - "express": "^4.18.2" + "express": "^4.21.2", + "helmet": "^8.0.0", + "sanitize-filename": "^1.6.3" }, "devDependencies": { - "nodemon": "^3.1.3" + "nodemon": "^3.1.9" } } diff --git a/cloud/server.js b/cloud/server.js index e269f26f..c983643f 100644 --- a/cloud/server.js +++ b/cloud/server.js @@ -4,6 +4,8 @@ const express = require('express'); const cors = require('cors'); const fs = require('fs'); const path = require('path'); +const sanitizeFilename = require('sanitize-filename'); +const helmet = require('helmet'); const app = express(); const port = process.env.PORT || 8080; @@ -14,6 +16,9 @@ const log = { debug: console.log, }; +// Recording max file size +const recMaxFileSize = 1 * 1024 * 1024 * 1024; // 1 GB + // Directory where recordings will be stored const recordingDirectory = path.join(__dirname, 'rec'); @@ -27,6 +32,7 @@ const corsOptions = { }; // Middleware +app.use(helmet()); app.use(express.json()); app.use(cors(corsOptions)); @@ -50,16 +56,39 @@ app.post('/recSync', (req, res) => { return res.status(400).send('Filename not provided'); } - if (!isValidRecFileNameFormat(fileName)) { - log.warn('[RecSync] - Invalid file name', fileName); + const safeFileName = sanitizeFilename(fileName); + if (safeFileName !== fileName || !isValidRecFileNameFormat(fileName)) { + log.warn('[RecSync] - Invalid file name:', fileName); return res.status(400).send('Invalid file name'); } ensureRecordingDirectoryExists(); - const filePath = path.join(recordingDirectory, fileName); + const filePath = path.resolve(recordingDirectory, fileName); + if (!filePath.startsWith(path.resolve(recordingDirectory))) { + log.warn('[RecSync] - Attempt to save file outside allowed directory:', fileName); + return res.status(400).send('Invalid file path'); + } + + if (!['application/octet-stream'].includes(req.headers['content-type'])) { + log.warn('[RecSync] - Invalid content type:', req.headers['content-type']); + return res.status(400).send('Invalid content type'); + } + const writeStream = fs.createWriteStream(filePath, { flags: 'a' }); + let receivedBytes = 0; + + req.on('data', (chunk) => { + receivedBytes += chunk.length; + if (receivedBytes > recMaxFileSize) { + req.destroy(); // Stop receiving data + writeStream.destroy(); // Stop writing data + log.warn('[RecSync] - File size exceeds limit:', fileName); + return res.status(413).send('File too large'); + } + }); + req.pipe(writeStream); writeStream.on('error', (err) => { diff --git a/package.json b/package.json index 5e7baf80..85700e93 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mirotalksfu", - "version": "1.7.19", + "version": "1.7.20", "description": "WebRTC SFU browser-based video calls", "main": "Server.js", "scripts": { @@ -58,7 +58,7 @@ }, "dependencies": { "@mattermost/client": "10.2.0", - "@sentry/node": "^8.52.1", + "@sentry/node": "^8.53.0", "axios": "^1.7.9", "colors": "1.4.0", "compression": "1.7.5", @@ -70,6 +70,7 @@ "express-openid-connect": "^2.17.1", "fluent-ffmpeg": "^2.1.3", "he": "^1.2.0", + "helmet": "^8.0.0", "httpolyglot": "0.1.2", "js-yaml": "^4.1.0", "jsdom": "^26.0.0", @@ -80,6 +81,7 @@ "nodemailer": "^6.10.0", "openai": "^4.81.0", "qs": "6.14.0", + "sanitize-filename": "^1.6.3", "socket.io": "4.8.1", "swagger-ui-express": "5.0.1", "uuid": "11.0.5" diff --git a/public/js/Brand.js b/public/js/Brand.js index 961971a8..ee2234fd 100644 --- a/public/js/Brand.js +++ b/public/js/Brand.js @@ -64,7 +64,7 @@ let BRAND = { }, about: { imageUrl: '../images/mirotalk-logo.gif', - title: 'WebRTC SFU v1.7.19', + title: 'WebRTC SFU v1.7.20', html: `