From 143e570c6ae9d22602f1ced729dc98393a6dc8e8 Mon Sep 17 00:00:00 2001 From: Miroslav Pejic Date: Tue, 14 Jan 2025 00:59:04 +0100 Subject: [PATCH] [mirotalk] - improve security, add unit-tests, add chatgpt img --- .github/workflows/build.yml | 20 +++- app/src/server.js | 19 +++- app/src/validate.js | 21 ++++ app/src/xss.js | 143 +++++++++++++---------- package.json | 16 ++- public/images/chatgpt.png | Bin 0 -> 3053 bytes public/js/client.js | 7 +- tests/test-validate.js | 71 ++++++++++++ tests/test-xss.js | 218 ++++++++++++++++++++++++++++++++++++ 9 files changed, 448 insertions(+), 67 deletions(-) create mode 100644 app/src/validate.js create mode 100644 public/images/chatgpt.png create mode 100644 tests/test-validate.js create mode 100644 tests/test-xss.js diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 64267a96..c612be19 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,8 +6,26 @@ on: - master jobs: - publish: + test: runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: '22.13.0' # LTS + + - name: Install dependencies + run: npm install + + - name: Run unit tests + run: npm test + + build: + runs-on: ubuntu-latest + needs: test # This ensures the build job only runs if the test job succeeds steps: - name: Checkout uses: actions/checkout@v3 diff --git a/app/src/server.js b/app/src/server.js index d36eed0b..fd925444 100755 --- a/app/src/server.js +++ b/app/src/server.js @@ -39,7 +39,7 @@ dependencies: { * @license For commercial use or closed source, contact us at license.mirotalk@gmail.com or purchase directly from CodeCanyon * @license CodeCanyon: https://codecanyon.net/item/mirotalk-p2p-webrtc-realtime-video-conferences/38376661 * @author Miroslav Pejic - miroslav.pejic.85@gmail.com - * @version 1.4.51 + * @version 1.4.55 * */ @@ -62,6 +62,7 @@ const fs = require('fs'); const checkXSS = require('./xss.js'); const ServerApi = require('./api'); const mattermostCli = require('./mattermost.js'); +const Validate = require('./validate'); const Host = require('./host'); const Logs = require('./logs'); const log = new Logs('server'); @@ -552,6 +553,12 @@ app.get('/join/', async (req, res) => { return res.status(401).json({ message: 'Direct Room Join: Missing mandatory room parameter!' }); } + if (!Validate.isValidRoomName(room)) { + return res.status(400).json({ + message: 'Invalid Room name!\nPath traversal pattern detected!', + }); + } + const allowRoomAccess = isAllowedRoomAccess('/join/params', req, hostCfg, peers, room); if (!allowRoomAccess && !token) { @@ -622,6 +629,11 @@ app.get('/join/:roomId', function (req, res) { return res.redirect('/'); } + if (!Validate.isValidRoomName(roomId)) { + log.warn('/join/:roomId invalid', roomId); + return res.redirect('/'); + } + const allowRoomAccess = isAllowedRoomAccess('/join/:roomId', req, hostCfg, peers, roomId); if (allowRoomAccess) { @@ -1176,6 +1188,11 @@ io.sockets.on('connect', async (socket) => { peer_info, } = config; + if (!Validate.isValidRoomName(channel)) { + log.warn('[' + socket.id + '] - Invalid room name', channel); + return socket.emit('unauthorized'); + } + if (channel in socket.channels) { return log.debug('[' + socket.id + '] [Warning] already joined', channel); } diff --git a/app/src/validate.js b/app/src/validate.js new file mode 100644 index 00000000..cc4e134f --- /dev/null +++ b/app/src/validate.js @@ -0,0 +1,21 @@ +'use strict'; + +const checkXSS = require('./xss.js'); + +function isValidRoomName(input) { + if (typeof input !== 'string') { + return false; + } + const room = checkXSS(input); + return !room ? false : !hasPathTraversal(room); +} + +function hasPathTraversal(input) { + const pathTraversalPattern = /(\.\.(\/|\\))+/; + return pathTraversalPattern.test(input); +} + +module.exports = { + isValidRoomName, + hasPathTraversal, +}; diff --git a/app/src/xss.js b/app/src/xss.js index e2388e8e..e251e03b 100644 --- a/app/src/xss.js +++ b/app/src/xss.js @@ -1,72 +1,101 @@ 'use strict'; -const xss = require('xss'); -const Logs = require('./logs'); -const log = new Logs('xss'); +const { JSDOM } = require('jsdom'); +const DOMPurify = require('dompurify'); +const he = require('he'); -/** - * Prevent XSS injection by client side - * - * @param {object} dataObject - * @returns sanitized object - */ +// Initialize DOMPurify with jsdom +const window = new JSDOM('').window; +const purify = DOMPurify(window); + +const Logger = require('./logs'); +const log = new Logger('Xss'); + +// Configure DOMPurify +purify.setConfig({ + ALLOWED_TAGS: ['a', 'img', 'div', 'span', 'svg', 'g', 'p'], // Allow specific tags + ALLOWED_ATTR: ['href', 'src', 'title', 'id', 'class', 'target'], // Allow specific attributes + ALLOWED_URI_REGEXP: /^(?!data:|javascript:|vbscript:|file:|view-source:).*/, // Disallow dangerous URIs +}); + +// Clean problematic attributes +function cleanAttributes(node) { + if (node.nodeType === window.Node.ELEMENT_NODE) { + // Remove dangerous attributes + const dangerousAttributes = ['onerror', 'onclick', 'onload', 'onmouseover', 'onfocus', 'onchange', 'oninput']; + dangerousAttributes.forEach((attr) => { + if (node.hasAttribute(attr)) { + node.removeAttribute(attr); + } + }); + + // Handle special cases for 'data:' URIs + const src = node.getAttribute('src'); + if (src && src.startsWith('data:')) { + node.removeAttribute('src'); + } + + // Remove unsafe 'style' attributes + if (node.hasAttribute('style')) { + const style = node.getAttribute('style'); + if (style.includes('javascript:') || style.includes('data:')) { + node.removeAttribute('style'); + } + } + + // Remove 'title' attribute if it contains dangerous content + if (node.hasAttribute('title')) { + const title = node.getAttribute('title'); + if (title.includes('javascript:') || title.includes('data:') || title.includes('onerror')) { + node.removeAttribute('title'); + } + } + } +} + +// Hook to clean specific attributes that can cause XSS +purify.addHook('beforeSanitizeAttributes', cleanAttributes); + +// Main function to check and sanitize data const checkXSS = (dataObject) => { try { - if (Array.isArray(dataObject)) { - if (Object.keys(dataObject).length > 0 && typeof dataObject[0] === 'object') { - dataObject.forEach((obj) => { - for (const key in obj) { - if (obj.hasOwnProperty(key)) { - let objectJson = objectToJSONString(obj[key]); - if (objectJson) { - let jsonString = xss(objectJson); - let jsonObject = JSONStringToObject(jsonString); - if (jsonObject) { - obj[key] = jsonObject; - } - } - } - } - }); - log.debug('XSS Array of Object sanitization done'); - return dataObject; - } - } else if (typeof dataObject === 'object') { - let objectJson = objectToJSONString(dataObject); - if (objectJson) { - let jsonString = xss(objectJson); - let jsonObject = JSONStringToObject(jsonString); - if (jsonObject) { - log.debug('XSS Object sanitization done'); - return jsonObject; - } - } - } else if (typeof dataObject === 'string' || dataObject instanceof String) { - log.debug('XSS String sanitization done'); - return xss(dataObject); - } - log.warn('XSS not sanitized', dataObject); - return dataObject; + return sanitizeData(dataObject); } catch (error) { - log.error('XSS error', { data: dataObject, error: error }); - return dataObject; + log.error('Sanitization error:', error); + return dataObject; // Return original data in case of error } }; -function objectToJSONString(dataObject) { - try { - return JSON.stringify(dataObject); - } catch (error) { - return false; - } +function needsDecoding(str) { + const urlEncodedPattern = /%[0-9A-Fa-f]{2}/g; + return urlEncodedPattern.test(str); } -function JSONStringToObject(jsonString) { - try { - return JSON.parse(jsonString); - } catch (error) { - return false; +// Recursively sanitize data based on its type +function sanitizeData(data) { + if (typeof data === 'string') { + // Decode HTML entities and URL encoded content + const decodedData = needsDecoding(data) ? he.decode(decodeURIComponent(data)) : he.decode(data); + return purify.sanitize(decodedData); } + + if (Array.isArray(data)) { + return data.map(sanitizeData); + } + + if (data && typeof data === 'object') { + return sanitizeObject(data); + } + + return data; // For numbers, booleans, null, undefined +} + +// Sanitize object properties +function sanitizeObject(obj) { + return Object.keys(obj).reduce((acc, key) => { + acc[key] = sanitizeData(obj[key]); + return acc; + }, {}); } module.exports = checkXSS; diff --git a/package.json b/package.json index eff6d869..246eac59 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { "name": "mirotalk", - "version": "1.4.51", + "version": "1.4.55", "description": "A free WebRTC browser-based video call", "main": "server.js", "scripts": { "start": "node app/src/server.js", "start-dev": "nodemon app/src/server.js", - "test": "echo \"Error: no test specified\" && exit 1", + "test": "mocha tests/*.js", "lint": "npx prettier --write .", "docker-build": "docker build --tag mirotalk/p2p:latest .", "docker-rmi": "docker images |grep '' |awk '{print $3}' |xargs docker rmi", @@ -48,9 +48,12 @@ "compression": "^1.7.5", "cors": "^2.8.5", "crypto-js": "^4.2.0", + "dompurify": "^3.2.3", "dotenv": "^16.4.7", "express": "^4.21.2", "express-openid-connect": "^2.17.1", + "he": "^1.2.0", + "jsdom": "^26.0.0", "jsonwebtoken": "^9.0.2", "js-yaml": "^4.1.0", "ngrok": "^5.0.0-beta.2", @@ -59,12 +62,15 @@ "qs": "^6.13.1", "socket.io": "^4.8.1", "swagger-ui-express": "^5.0.1", - "uuid": "11.0.5", - "xss": "^1.0.15" + "uuid": "11.0.5" }, "devDependencies": { + "mocha": "^11.0.1", "node-fetch": "^3.3.2", "nodemon": "^3.1.9", - "prettier": "3.4.2" + "prettier": "3.4.2", + "proxyquire": "^2.1.3", + "should": "^13.2.3", + "sinon": "^19.0.2" } } diff --git a/public/images/chatgpt.png b/public/images/chatgpt.png new file mode 100644 index 0000000000000000000000000000000000000000..cced3a3a880b5f0b7ffa4c8c6f351b4ec3101b59 GIT binary patch literal 3053 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 P2P v1.4.51', + title: 'WebRTC P2P v1.4.55', imageAlt: 'mirotalk-about', imageUrl: images.about, customClass: { image: 'img-about' }, diff --git a/tests/test-validate.js b/tests/test-validate.js new file mode 100644 index 00000000..b0ff5ac3 --- /dev/null +++ b/tests/test-validate.js @@ -0,0 +1,71 @@ +'use strict'; + +// npx mocha test-validator.js + +require('should'); + +const checkValidator = require('../app/src/validate'); + +describe('test-validate', () => { + 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(); + checkValidator.isValidRoomName([]).should.be.false(); + checkValidator.isValidRoomName(null).should.be.false(); + 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(); + checkValidator.isValidRoomName('Room_123').should.be.true(); + checkValidator.isValidRoomName('30521HungryHat').should.be.true(); + checkValidator.isValidRoomName('dbc4a9d9-6879-479a-b8fe-cedaad176b0d').should.be.true(); + }); + + it('should return false for room name with path traversal', () => { + checkValidator.isValidRoomName('../etc/passwd').should.be.false(); + checkValidator.isValidRoomName('..\\etc\\passwd').should.be.false(); + checkValidator.isValidRoomName('Room/../../etc').should.be.false(); + checkValidator.isValidRoomName('Room\\..\\..\\etc').should.be.false(); + }); + + it('should return true for room names with special characters that do not imply path traversal', () => { + checkValidator.isValidRoomName('Room_@!#$%^&*()').should.be.true(); + checkValidator.isValidRoomName('Room-Name').should.be.true(); + checkValidator.isValidRoomName('Room.Name').should.be.true(); + }); + }); + + describe('3. Handle path traversal', () => { + it('should return false for strings without path traversal', () => { + checkValidator.hasPathTraversal('Room1').should.be.false(); + checkValidator.hasPathTraversal('Rec_Test.webm').should.be.false(); + checkValidator.hasPathTraversal('simple/path').should.be.false(); + }); + + it('should return true for strings with path traversal', () => { + checkValidator.hasPathTraversal('../etc/passwd').should.be.true(); + checkValidator.hasPathTraversal('..\\etc\\passwd').should.be.true(); + checkValidator.hasPathTraversal('Room/../../etc').should.be.true(); + checkValidator.hasPathTraversal('Room\\..\\..\\etc').should.be.true(); + }); + + it('should return false for strings with ".." that do not indicate path traversal', () => { + checkValidator.hasPathTraversal('Room..').should.be.false(); + checkValidator.hasPathTraversal('Rec..webm').should.be.false(); + checkValidator.hasPathTraversal('NoPathTraversalHere..').should.be.false(); + }); + + it('should return true for complex path traversal patterns', () => { + checkValidator.hasPathTraversal('....//').should.be.true(); + checkValidator.hasPathTraversal('..\\..\\').should.be.true(); + checkValidator.hasPathTraversal('.../../').should.be.true(); + }); + }); +}); diff --git a/tests/test-xss.js b/tests/test-xss.js new file mode 100644 index 00000000..03f50080 --- /dev/null +++ b/tests/test-xss.js @@ -0,0 +1,218 @@ +'use strict'; + +// npx mocha test-xss.js + +require('should'); + +const checkXSS = require('../app/src/xss'); + +describe('test-xss', () => { + describe('1. Basic Data Types Handling', () => { + it('should return numbers and booleans unchanged', () => { + checkXSS(42).should.equal(42); + checkXSS(true).should.equal(true); + }); + + it('should return null and undefined unchanged', () => { + should.not.exist(checkXSS(null)); + should.not.exist(checkXSS(undefined)); + }); + }); + + describe('2. Simple String Handling', () => { + it('should sanitize strings with XSS injections', () => { + const maliciousString = ''; + const sanitizedString = checkXSS(maliciousString); + sanitizedString.should.not.containEql(''); + }); + + it('should sanitize complex XSS injections', () => { + const complexString = ''; + const sanitizedString = checkXSS(complexString); + sanitizedString.should.not.containEql('onload'); + sanitizedString.should.equal(''); + }); + + it('should sanitize HTML attributes', () => { + const maliciousHtml = 'click me'; + const sanitizedHtml = checkXSS(maliciousHtml); + sanitizedHtml.should.not.containEql('javascript:'); + sanitizedHtml.should.containEql('click me'); + }); + + it('should sanitize embedded scripts in HTML', () => { + const maliciousHtml = '
'; + const sanitizedHtml = checkXSS(maliciousHtml); + sanitizedHtml.should.not.containEql('', + key2: 'normal string', + }; + const sanitizedObject = checkXSS(maliciousObject); + sanitizedObject.key1.should.not.containEql(''); + sanitizedObject.key2.should.equal('normal string'); + }); + + it('should sanitize arrays with XSS injections', () => { + const maliciousArray = ['', 'normal string']; + const sanitizedArray = checkXSS(maliciousArray); + sanitizedArray[0].should.not.containEql(''); + sanitizedArray[1].should.equal('normal string'); + }); + + it('should handle nested objects and arrays with XSS injections', () => { + const nestedData = { + key1: [ + '', + { + key2: '', + }, + ], + }; + const sanitizedData = checkXSS(nestedData); + sanitizedData.key1[0].should.not.containEql(''); + sanitizedData.key1[1].key2.should.not.containEql('onerror'); + sanitizedData.key1[1].key2.should.equal(''); + }); + + it('should handle XSS in nested HTML elements', () => { + const nestedXss = '
Click me
'; + const sanitizedNestedXss = checkXSS(nestedXss); + sanitizedNestedXss.should.not.containEql('onclick'); + sanitizedNestedXss.should.containEql('
Click me
'); + }); + + it('should handle XSS through malicious attributes in different tags', () => { + const maliciousAttributes = + 'Link'; + const sanitizedAttributes = checkXSS(maliciousAttributes); + sanitizedAttributes.should.not.containEql('onclick'); + sanitizedAttributes.should.not.containEql('javascript:'); + sanitizedAttributes.should.not.containEql('alert'); + }); + }); + + describe('4. Handling Specific Formats (JSON, Base64, etc.)', () => { + it('should handle XSS in JSON data', () => { + const maliciousJson = '{"key": ""}'; + const sanitizedJson = checkXSS(JSON.parse(maliciousJson)); + sanitizedJson.key.should.not.containEql('onerror'); + sanitizedJson.key.should.equal(''); + }); + + it('should sanitize base64 encoded content', () => { + const maliciousBase64 = ''; + const sanitizedBase64 = checkXSS(maliciousBase64); + sanitizedBase64.should.not.containEql('onload'); + sanitizedBase64.should.equal(''); + }); + + it('should sanitize encoded HTML entities', () => { + const encodedHtmlEntities = '<script>alert('xss')</script>'; + const sanitizedEntities = checkXSS(encodedHtmlEntities); + sanitizedEntities.should.not.containEql(''; + const sanitizedSvgXss = checkXSS(svgXss); + sanitizedSvgXss.should.not.containEql(''; + const sanitizedDynamicXss = checkXSS(dynamicXss); + sanitizedDynamicXss.should.not.containEql('onerror'); + sanitizedDynamicXss.should.containEql('
'); + }); + }); + + describe('8. Handling Mixed Content', () => { + it('should sanitize mixed content', () => { + const mixedContent = '
Normal text more text
'; + const sanitizedContent = checkXSS(mixedContent); + sanitizedContent.should.not.containEql('