[mirotalksfu] - improve security, update dep
هذا الالتزام موجود في:
@@ -18,14 +18,14 @@ class HtmlInjector {
|
|||||||
// Function to get dynamic data for injection (e.g., OG data, title, etc.)
|
// Function to get dynamic data for injection (e.g., OG data, title, etc.)
|
||||||
getInjectData() {
|
getInjectData() {
|
||||||
return {
|
return {
|
||||||
OG_TYPE: this.config.og?.type || 'app-webrtc',
|
OG_TYPE: this.config?.og?.type || 'app-webrtc',
|
||||||
OG_SITE_NAME: this.config.og?.siteName || 'MiroTalk SFU',
|
OG_SITE_NAME: this.config?.og?.siteName || 'MiroTalk SFU',
|
||||||
OG_TITLE: this.config.og?.title || 'Click the link to make a call.',
|
OG_TITLE: this.config?.og?.title || 'Click the link to make a call.',
|
||||||
OG_DESCRIPTION:
|
OG_DESCRIPTION:
|
||||||
this.config.og?.description ||
|
this.config?.og?.description ||
|
||||||
'MiroTalk SFU calling provides real-time video calls, messaging and screen sharing.',
|
'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_IMAGE: this.config?.og?.image || 'https://sfu.mirotalk.com/images/mirotalksfu.png',
|
||||||
OG_URL: this.config.og?.url || 'https://sfu.mirotalk.com',
|
OG_URL: this.config?.og?.url || 'https://sfu.mirotalk.com',
|
||||||
// Add more data here as needed with fallbacks
|
// Add more data here as needed with fallbacks
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 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
|
* @license CodeCanyon: https://codecanyon.net/item/mirotalk-sfu-webrtc-realtime-video-conferences/40769970
|
||||||
* @author Miroslav Pejic - miroslav.pejic.85@gmail.com
|
* @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 ngrok = require('ngrok');
|
||||||
const jwt = require('jsonwebtoken');
|
const jwt = require('jsonwebtoken');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
const sanitizeFilename = require('sanitize-filename');
|
||||||
|
const helmet = require('helmet');
|
||||||
const config = require('./config');
|
const config = require('./config');
|
||||||
const checkXSS = require('./XSS.js');
|
const checkXSS = require('./XSS');
|
||||||
const Host = require('./Host');
|
const Host = require('./Host');
|
||||||
const Room = require('./Room');
|
const Room = require('./Room');
|
||||||
const Peer = require('./Peer');
|
const Peer = require('./Peer');
|
||||||
@@ -226,16 +228,28 @@ const OIDC = config.oidc ? config.oidc : { enabled: false };
|
|||||||
const dir = {
|
const dir = {
|
||||||
public: path.join(__dirname, '../../', 'public'),
|
public: path.join(__dirname, '../../', 'public'),
|
||||||
rec: path.join(__dirname, '../', config?.server?.recording?.dir ? config.server.recording.dir + '/' : 'rec/'),
|
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;
|
const serverRecordingEnabled = config?.server?.recording?.enabled;
|
||||||
if (serverRecordingEnabled) {
|
if (serverRecordingEnabled) {
|
||||||
|
log.debug('Server Recording enabled creating dir', dir.rtmp);
|
||||||
if (!fs.existsSync(dir.rec)) {
|
if (!fs.existsSync(dir.rec)) {
|
||||||
fs.mkdirSync(dir.rec, { recursive: true });
|
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
|
// html views
|
||||||
const views = {
|
const views = {
|
||||||
html: path.join(__dirname, '../../public/views'),
|
html: path.join(__dirname, '../../public/views'),
|
||||||
@@ -340,6 +354,8 @@ function OIDCAuth(req, res, next) {
|
|||||||
|
|
||||||
function startServer() {
|
function startServer() {
|
||||||
// Start the app
|
// 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(express.static(dir.public));
|
||||||
app.use(cors(corsOptions));
|
app.use(cors(corsOptions));
|
||||||
app.use(compression());
|
app.use(compression());
|
||||||
@@ -769,8 +785,10 @@ function startServer() {
|
|||||||
return res.status(400).send('Filename not provided');
|
return res.status(400).send('Filename not provided');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Validator.isValidRecFileNameFormat(fileName)) {
|
// Sanitize and validate filename
|
||||||
log.warn('[RecSync] - Invalid file name', 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');
|
return res.status(400).send('Invalid file name');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -782,11 +800,37 @@ function startServer() {
|
|||||||
return res.status(400).send('Invalid file name');
|
return res.status(400).send('Invalid file name');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure directory exists
|
||||||
if (!fs.existsSync(dir.rec)) {
|
if (!fs.existsSync(dir.rec)) {
|
||||||
fs.mkdirSync(dir.rec, { recursive: true });
|
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' });
|
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);
|
req.pipe(writeStream);
|
||||||
|
|
||||||
@@ -841,7 +885,6 @@ function startServer() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.get('/rtmpEnabled', (req, res) => {
|
app.get('/rtmpEnabled', (req, res) => {
|
||||||
const rtmpEnabled = rtmpCfg && rtmpCfg.enabled;
|
|
||||||
log.debug('RTMP enabled', rtmpEnabled);
|
log.debug('RTMP enabled', rtmpEnabled);
|
||||||
res.json({ enabled: rtmpEnabled });
|
res.json({ enabled: rtmpEnabled });
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ module.exports = {
|
|||||||
enabled: false,
|
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).
|
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',
|
dir: 'rec',
|
||||||
|
maxFileSize: 1 * 1024 * 1024 * 1024, // 1 GB
|
||||||
},
|
},
|
||||||
rtmp: {
|
rtmp: {
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -15,9 +15,11 @@
|
|||||||
"license": "AGPLv3",
|
"license": "AGPLv3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cors": "2.8.5",
|
"cors": "2.8.5",
|
||||||
"express": "^4.18.2"
|
"express": "^4.21.2",
|
||||||
|
"helmet": "^8.0.0",
|
||||||
|
"sanitize-filename": "^1.6.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"nodemon": "^3.1.3"
|
"nodemon": "^3.1.9"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ const express = require('express');
|
|||||||
const cors = require('cors');
|
const cors = require('cors');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const sanitizeFilename = require('sanitize-filename');
|
||||||
|
const helmet = require('helmet');
|
||||||
const app = express();
|
const app = express();
|
||||||
const port = process.env.PORT || 8080;
|
const port = process.env.PORT || 8080;
|
||||||
|
|
||||||
@@ -14,6 +16,9 @@ const log = {
|
|||||||
debug: console.log,
|
debug: console.log,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Recording max file size
|
||||||
|
const recMaxFileSize = 1 * 1024 * 1024 * 1024; // 1 GB
|
||||||
|
|
||||||
// Directory where recordings will be stored
|
// Directory where recordings will be stored
|
||||||
const recordingDirectory = path.join(__dirname, 'rec');
|
const recordingDirectory = path.join(__dirname, 'rec');
|
||||||
|
|
||||||
@@ -27,6 +32,7 @@ const corsOptions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Middleware
|
// Middleware
|
||||||
|
app.use(helmet());
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(cors(corsOptions));
|
app.use(cors(corsOptions));
|
||||||
|
|
||||||
@@ -50,16 +56,39 @@ app.post('/recSync', (req, res) => {
|
|||||||
return res.status(400).send('Filename not provided');
|
return res.status(400).send('Filename not provided');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isValidRecFileNameFormat(fileName)) {
|
const safeFileName = sanitizeFilename(fileName);
|
||||||
log.warn('[RecSync] - Invalid file name', fileName);
|
if (safeFileName !== fileName || !isValidRecFileNameFormat(fileName)) {
|
||||||
|
log.warn('[RecSync] - Invalid file name:', fileName);
|
||||||
return res.status(400).send('Invalid file name');
|
return res.status(400).send('Invalid file name');
|
||||||
}
|
}
|
||||||
|
|
||||||
ensureRecordingDirectoryExists();
|
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' });
|
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);
|
req.pipe(writeStream);
|
||||||
|
|
||||||
writeStream.on('error', (err) => {
|
writeStream.on('error', (err) => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mirotalksfu",
|
"name": "mirotalksfu",
|
||||||
"version": "1.7.19",
|
"version": "1.7.20",
|
||||||
"description": "WebRTC SFU browser-based video calls",
|
"description": "WebRTC SFU browser-based video calls",
|
||||||
"main": "Server.js",
|
"main": "Server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mattermost/client": "10.2.0",
|
"@mattermost/client": "10.2.0",
|
||||||
"@sentry/node": "^8.52.1",
|
"@sentry/node": "^8.53.0",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"colors": "1.4.0",
|
"colors": "1.4.0",
|
||||||
"compression": "1.7.5",
|
"compression": "1.7.5",
|
||||||
@@ -70,6 +70,7 @@
|
|||||||
"express-openid-connect": "^2.17.1",
|
"express-openid-connect": "^2.17.1",
|
||||||
"fluent-ffmpeg": "^2.1.3",
|
"fluent-ffmpeg": "^2.1.3",
|
||||||
"he": "^1.2.0",
|
"he": "^1.2.0",
|
||||||
|
"helmet": "^8.0.0",
|
||||||
"httpolyglot": "0.1.2",
|
"httpolyglot": "0.1.2",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"jsdom": "^26.0.0",
|
"jsdom": "^26.0.0",
|
||||||
@@ -80,6 +81,7 @@
|
|||||||
"nodemailer": "^6.10.0",
|
"nodemailer": "^6.10.0",
|
||||||
"openai": "^4.81.0",
|
"openai": "^4.81.0",
|
||||||
"qs": "6.14.0",
|
"qs": "6.14.0",
|
||||||
|
"sanitize-filename": "^1.6.3",
|
||||||
"socket.io": "4.8.1",
|
"socket.io": "4.8.1",
|
||||||
"swagger-ui-express": "5.0.1",
|
"swagger-ui-express": "5.0.1",
|
||||||
"uuid": "11.0.5"
|
"uuid": "11.0.5"
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ let BRAND = {
|
|||||||
},
|
},
|
||||||
about: {
|
about: {
|
||||||
imageUrl: '../images/mirotalk-logo.gif',
|
imageUrl: '../images/mirotalk-logo.gif',
|
||||||
title: '<strong>WebRTC SFU v1.7.19</strong>',
|
title: '<strong>WebRTC SFU v1.7.20</strong>',
|
||||||
html: `
|
html: `
|
||||||
<button
|
<button
|
||||||
id="support-button"
|
id="support-button"
|
||||||
|
|||||||
@@ -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 license.mirotalk@gmail.com or purchase directly via CodeCanyon
|
* @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
|
* @license CodeCanyon: https://codecanyon.net/item/mirotalk-sfu-webrtc-realtime-video-conferences/40769970
|
||||||
* @author Miroslav Pejic - miroslav.pejic.85@gmail.com
|
* @author Miroslav Pejic - miroslav.pejic.85@gmail.com
|
||||||
* @version 1.7.19
|
* @version 1.7.20
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -4903,13 +4903,9 @@ function showAbout() {
|
|||||||
Swal.fire({
|
Swal.fire({
|
||||||
background: swalBackground,
|
background: swalBackground,
|
||||||
position: 'center',
|
position: 'center',
|
||||||
imageUrl: BRAND.about?.imageUrl && BRAND.about.imageUrl.trim() !== ''
|
imageUrl: BRAND.about?.imageUrl && BRAND.about.imageUrl.trim() !== '' ? BRAND.about.imageUrl : image.about,
|
||||||
? BRAND.about.imageUrl
|
|
||||||
: image.about,
|
|
||||||
customClass: { image: 'img-about' },
|
customClass: { image: 'img-about' },
|
||||||
title: BRAND.about?.title && BRAND.about.title.trim() !== ''
|
title: BRAND.about?.title && BRAND.about.title.trim() !== '' ? BRAND.about.title : 'WebRTC SFU v1.7.20',
|
||||||
? BRAND.about.title
|
|
||||||
: 'WebRTC SFU v1.7.19',
|
|
||||||
html: `
|
html: `
|
||||||
<br />
|
<br />
|
||||||
<div id="about">
|
<div id="about">
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
* @license For commercial or closed source, contact us at license.mirotalk@gmail.com or purchase directly via CodeCanyon
|
* @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
|
* @license CodeCanyon: https://codecanyon.net/item/mirotalk-sfu-webrtc-realtime-video-conferences/40769970
|
||||||
* @author Miroslav Pejic - miroslav.pejic.85@gmail.com
|
* @author Miroslav Pejic - miroslav.pejic.85@gmail.com
|
||||||
* @version 1.7.19
|
* @version 1.7.20
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
المرجع في مشكلة جديدة
حظر مستخدم