diff --git a/app/src/Server.js b/app/src/Server.js index c17a61fa..f63ce47c 100644 --- a/app/src/Server.js +++ b/app/src/Server.js @@ -7,7 +7,7 @@      ██ ██     ██   ██  ██  ██  ██     ██   ██  ███████ ███████ ██  ██   ████   ███████ ██  ██                                        -dependencies: { +prod dependencies: { @ffmpeg-installer/ffmpeg: https://www.npmjs.com/package/@ffmpeg-installer/ffmpeg @sentry/node : https://www.npmjs.com/package/@sentry/node axios : https://www.npmjs.com/package/axios @@ -16,12 +16,15 @@ dependencies: { colors : https://www.npmjs.com/package/colors cors : https://www.npmjs.com/package/cors crypto-js : https://www.npmjs.com/package/crypto-js + dompurify : https://www.npmjs.com/package/dompurify express : https://www.npmjs.com/package/express express-openid-connect : https://www.npmjs.com/package/express-openid-connect fluent-ffmpeg : https://www.npmjs.com/package/fluent-ffmpeg + he : https://www.npmjs.com/package/he httpolyglot : https://www.npmjs.com/package/httpolyglot - jsonwebtoken : https://www.npmjs.com/package/jsonwebtoken js-yaml : https://www.npmjs.com/package/js-yaml + jsdom : https://www.npmjs.com/package/jsdom + jsonwebtoken : https://www.npmjs.com/package/jsonwebtoken mediasoup : https://www.npmjs.com/package/mediasoup mediasoup-client : https://www.npmjs.com/package/mediasoup-client ngrok : https://www.npmjs.com/package/ngrok @@ -30,7 +33,16 @@ dependencies: { socket.io : https://www.npmjs.com/package/socket.io swagger-ui-express : https://www.npmjs.com/package/swagger-ui-express uuid : https://www.npmjs.com/package/uuid - xss : https://www.npmjs.com/package/xss +} + +dev dependencies: { + mocha : https://www.npmjs.com/package/mocha + node-fetch : https://www.npmjs.com/package/node-fetch + nodemon : https://www.npmjs.com/package/nodemon + prettier : https://www.npmjs.com/package/prettier + proxyquire : https://www.npmjs.com/package/proxyquire + should : https://www.npmjs.com/package/should + sinon : https://www.npmjs.com/package/sinon } */ @@ -43,7 +55,7 @@ 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.5.60 + * @version 1.5.61 * */ diff --git a/app/src/Validator.js b/app/src/Validator.js index 8e78f851..b114d18f 100644 --- a/app/src/Validator.js +++ b/app/src/Validator.js @@ -1,10 +1,13 @@ 'use strict'; +const checkXSS = require('./XSS.js'); + function isValidRoomName(input) { if (typeof input !== 'string') { return false; } - return !hasPathTraversal(input); + const room = checkXSS(input); + return !room ? false : !hasPathTraversal(room); } function isValidRecFileNameFormat(input) { diff --git a/app/src/XSS.js b/app/src/XSS.js index 6d62d7bf..c734d32b 100644 --- a/app/src/XSS.js +++ b/app/src/XSS.js @@ -66,11 +66,16 @@ const checkXSS = (dataObject) => { } }; +function needsDecoding(str) { + const urlEncodedPattern = /%[0-9A-Fa-f]{2}/g; + return urlEncodedPattern.test(str); +} + // Recursively sanitize data based on its type function sanitizeData(data) { if (typeof data === 'string') { // Decode HTML entities and URL encoded content - const decodedData = he.decode(decodeURIComponent(data)); + const decodedData = needsDecoding(data) ? he.decode(decodeURIComponent(data)) : he.decode(data); return purify.sanitize(decodedData); } diff --git a/package.json b/package.json index 4a8e6f90..61196e1b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mirotalksfu", - "version": "1.5.60", + "version": "1.5.61", "description": "WebRTC SFU browser-based video calls", "main": "Server.js", "scripts": { @@ -28,7 +28,7 @@ "nms-stop": "docker-compose -f rtmpServers/node-media-server/docker-compose.yml down", "nms-restart": "docker-compose -f rtmpServers/node-media-server/docker-compose.yml down && docker-compose -f rtmpServers/node-media-server/docker-compose.yml up -d", "nms-logs": "docker logs -f mirotalk-nms", - "unit-test": "npx mocha tests/checkXSS.js" + "unit-tests": "npx mocha tests" }, "repository": { "type": "git", @@ -89,6 +89,8 @@ "node-fetch": "^3.3.2", "nodemon": "^3.1.4", "prettier": "3.3.3", - "should": "^13.2.3" + "proxyquire": "^2.1.3", + "should": "^13.2.3", + "sinon": "^18.0.0" } } diff --git a/public/js/Room.js b/public/js/Room.js index 486edd4f..35a1d05d 100644 --- a/public/js/Room.js +++ b/public/js/Room.js @@ -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 CodeCanyon: https://codecanyon.net/item/mirotalk-sfu-webrtc-realtime-video-conferences/40769970 * @author Miroslav Pejic - miroslav.pejic.85@gmail.com - * @version 1.5.60 + * @version 1.5.61 * */ @@ -4444,7 +4444,7 @@ function showAbout() { imageUrl: image.about, customClass: { image: 'img-about' }, position: 'center', - title: 'WebRTC SFU v1.5.60', + title: 'WebRTC SFU v1.5.61', html: `
diff --git a/public/js/RoomClient.js b/public/js/RoomClient.js index 4f372af5..2dcef8ce 100644 --- a/public/js/RoomClient.js +++ b/public/js/RoomClient.js @@ -9,7 +9,7 @@ * @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.5.60 + * @version 1.5.61 * */ diff --git a/tests/checkServerAPI.js b/tests/checkServerAPI.js new file mode 100644 index 00000000..593a9df0 --- /dev/null +++ b/tests/checkServerAPI.js @@ -0,0 +1,187 @@ +'use strict'; + +// npx mocha checkServerApi.js + +require('should'); + +const sinon = require('sinon'); +const proxyquire = require('proxyquire'); +const jwt = require('jsonwebtoken'); +const CryptoJS = require('crypto-js'); +const ServerApi = require('../app/src/ServerApi'); +const config = require('../app/src/config'); + +describe('checkServerAPI', () => { + let serverApi; + const host = 'example.com'; + const authorization = 'secret-key'; + const apiKeySecret = 'secret-key'; + + beforeEach(() => { + // Mocking config values + sinon.stub(config.api, 'keySecret').value(apiKeySecret); + serverApi = new ServerApi(host, authorization); + }); + + afterEach(() => { + sinon.restore(); + }); + + describe('isAuthorized', () => { + it('should return true when authorization matches the api key secret', () => { + serverApi.isAuthorized().should.be.true(); + }); + + it('should return false when authorization does not match the api key secret', () => { + serverApi = new ServerApi(host, 'wrong-key'); + serverApi.isAuthorized().should.be.false(); + }); + }); + + describe('getMeetings', () => { + it('should return formatted meetings with peer information', () => { + const roomList = new Map([ + [ + 'room1', + { + peers: new Map([ + [ + 'peer1', + { + peer_info: { + peer_name: 'John Doe', + peer_presenter: true, + peer_video: true, + peer_audio: true, + peer_screen: false, + peer_hand: false, + os_name: 'Windows', + os_version: '10', + browser_name: 'Chrome', + browser_version: '100', + }, + }, + ], + ]), + }, + ], + ]); + + const result = serverApi.getMeetings(roomList); + result.should.deepEqual([ + { + roomId: 'room1', + peers: [ + { + name: 'John Doe', + presenter: true, + video: true, + audio: true, + screen: false, + hand: false, + os: 'Windows 10', + browser: 'Chrome 100', + }, + ], + }, + ]); + }); + + it('should handle rooms with no peers', () => { + const roomList = new Map([['room1', { peers: new Map() }]]); + const result = serverApi.getMeetings(roomList); + result.should.deepEqual([{ roomId: 'room1', peers: [] }]); + }); + }); + + describe('getMeetingURL', () => { + it('should return a meeting URL with a generated UUID', () => { + const uuidV4Stub = sinon.stub().returns('12345'); + const ServerApi = proxyquire('../app/src/ServerApi', { + uuid: { v4: uuidV4Stub }, + }); + + serverApi = new ServerApi(host, authorization); + + const result = serverApi.getMeetingURL(); + result.should.equal('https://example.com/join/12345'); + }); + }); + + describe('getJoinURL', () => { + it('should return a valid join URL with the given data', () => { + const data = { + room: 'room1', + roomPassword: 'password123', + name: 'John Doe', + audio: true, + video: false, + screen: false, + hide: false, + notify: false, + token: { username: 'user', password: 'pass', presenter: true, expire: '1h' }, + }; + + const tokenStub = sinon.stub(serverApi, 'getToken').returns('testToken'); + + const result = serverApi.getJoinURL(data); + result.should.equal( + 'https://example.com/join?room=room1&roomPassword=password123&name=John%20Doe&audio=true&video=false&screen=false&hide=false¬ify=false&token=testToken', + ); + + tokenStub.restore(); + }); + + it('should use default values when data is not provided', () => { + const randomStub = sinon.stub().returns('123456'); + const uuidV4Stub = sinon.stub().returns('room1'); + const ServerApi = proxyquire('../app/src/ServerApi', { + uuid: { v4: uuidV4Stub }, + }); + + serverApi = new ServerApi(host, authorization); + sinon.stub(serverApi, 'getRandomNumber').callsFake(randomStub); + + const result = serverApi.getJoinURL({}); + result.should.equal( + 'https://example.com/join?room=room1&roomPassword=false&name=User-123456&audio=false&video=false&screen=false&hide=false¬ify=false', + ); + }); + }); + + describe('getToken', () => { + it('should return an encrypted JWT token', () => { + const tokenData = { username: 'user', password: 'pass', presenter: true, expire: '1h' }; + const signStub = sinon.stub(jwt, 'sign').returns('jwtToken'); + const encryptStub = sinon.stub(CryptoJS.AES, 'encrypt').returns({ toString: () => 'encryptedPayload' }); + + const result = serverApi.getToken(tokenData); + result.should.equal('jwtToken'); + + signStub + .calledWith({ data: 'encryptedPayload' }, 'mirotalksfu_jwt_secret', { expiresIn: '1h' }) + .should.be.true(); + encryptStub + .calledWith( + JSON.stringify({ username: 'user', password: 'pass', presenter: 'true' }), + 'mirotalksfu_jwt_secret', + ) + .should.be.true(); + + signStub.restore(); + encryptStub.restore(); + }); + + it('should return an empty string if no token data is provided', () => { + const result = serverApi.getToken(null); + result.should.equal(''); + }); + }); + + describe('getRandomNumber', () => { + it('should return a random number between 0 and 999999', () => { + const result = serverApi.getRandomNumber(); + result.should.be.within(0, 999999); + }); + }); +}); diff --git a/tests/checkValidator.js b/tests/checkValidator.js index 3c4cf3d8..91d8d44c 100644 --- a/tests/checkValidator.js +++ b/tests/checkValidator.js @@ -7,7 +7,7 @@ require('should'); const checkValidator = require('../app/src/Validator'); describe('checkValidator', () => { - describe('1. Handling valid room name', () => { + describe('1. Handling invalid room name', () => { it('should return false for non-string inputs', () => { checkValidator.isValidRoomName(123).should.be.false(); checkValidator.isValidRoomName({}).should.be.false(); @@ -16,6 +16,10 @@ describe('checkValidator', () => { checkValidator.isValidRoomName(undefined).should.be.false(); }); + it('should return false for xss injection inputs', () => { + checkValidator.isValidRoomName('').should.be.false(); + }); + it('should return true for valid room name', () => { checkValidator.isValidRoomName('Room1').should.be.true(); checkValidator.isValidRoomName('ConferenceRoom').should.be.true();