Skip to content

Commit

Permalink
[mirotalksfu] - improve security, update dep
Browse files Browse the repository at this point in the history
  • Loading branch information
miroslavpejic85 committed Jan 31, 2025
1 parent 6bf0908 commit 1727e64
Show file tree
Hide file tree
Showing 9 changed files with 105 additions and 32 deletions.
12 changes: 6 additions & 6 deletions app/src/HtmlInjector.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
}
Expand Down
57 changes: 50 additions & 7 deletions app/src/Server.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ dev dependencies: {
* @license For commercial or closed source, contact us at [email protected] or purchase directly via CodeCanyon
* @license CodeCanyon: https://codecanyon.net/item/mirotalk-sfu-webrtc-realtime-video-conferences/40769970
* @author Miroslav Pejic - [email protected]
* @version 1.7.19
* @version 1.7.20
*
*/

Expand All @@ -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');
Expand Down Expand Up @@ -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'),
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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');
}

Expand All @@ -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);

Expand Down Expand Up @@ -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 });
});
Expand Down
1 change: 1 addition & 0 deletions app/src/config.template.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
/*
Expand Down
6 changes: 4 additions & 2 deletions cloud/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
35 changes: 32 additions & 3 deletions cloud/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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');

Expand All @@ -27,6 +32,7 @@ const corsOptions = {
};

// Middleware
app.use(helmet());
app.use(express.json());
app.use(cors(corsOptions));

Expand All @@ -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) => {
Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion public/js/Brand.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ let BRAND = {
},
about: {
imageUrl: '../images/mirotalk-logo.gif',
title: '<strong>WebRTC SFU v1.7.19</strong>',
title: '<strong>WebRTC SFU v1.7.20</strong>',
html: `
<button
id="support-button"
Expand Down
16 changes: 6 additions & 10 deletions public/js/Room.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ if (location.href.substr(0, 5) !== 'https') location.href = 'https' + location.h
* @license For commercial or closed source, contact us at [email protected] or purchase directly via CodeCanyon
* @license CodeCanyon: https://codecanyon.net/item/mirotalk-sfu-webrtc-realtime-video-conferences/40769970
* @author Miroslav Pejic - [email protected]
* @version 1.7.19
* @version 1.7.20
*
*/

Expand Down Expand Up @@ -4903,19 +4903,15 @@ function showAbout() {
Swal.fire({
background: swalBackground,
position: 'center',
imageUrl: BRAND.about?.imageUrl && BRAND.about.imageUrl.trim() !== ''
? BRAND.about.imageUrl
: image.about,
imageUrl: BRAND.about?.imageUrl && BRAND.about.imageUrl.trim() !== '' ? BRAND.about.imageUrl : image.about,
customClass: { image: 'img-about' },
title: BRAND.about?.title && BRAND.about.title.trim() !== ''
? BRAND.about.title
: 'WebRTC SFU v1.7.19',
title: BRAND.about?.title && BRAND.about.title.trim() !== '' ? BRAND.about.title : 'WebRTC SFU v1.7.20',
html: `
<br />
<div id="about">
${
BRAND.about?.html && BRAND.about.html.trim() !== ''
? BRAND.about.html
BRAND.about?.html && BRAND.about.html.trim() !== ''
? BRAND.about.html
: `
<button
id="support-button"
Expand Down Expand Up @@ -4951,4 +4947,4 @@ function showAbout() {
showClass: { popup: 'animate__animated animate__fadeInDown' },
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
});
}
}
2 changes: 1 addition & 1 deletion public/js/RoomClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
* @license For commercial or closed source, contact us at [email protected] or purchase directly via CodeCanyon
* @license CodeCanyon: https://codecanyon.net/item/mirotalk-sfu-webrtc-realtime-video-conferences/40769970
* @author Miroslav Pejic - [email protected]
* @version 1.7.19
* @version 1.7.20
*
*/

Expand Down

0 comments on commit 1727e64

Please sign in to comment.