[mirotalksfu] - Improve security, add tests

هذا الالتزام موجود في:
Miroslav Pejic
2024-08-16 01:20:09 +02:00
الأصل 8d7901e7d0
التزام 7287363a86
6 ملفات معدلة مع 311 إضافات و58 حذوفات

عرض الملف

@@ -43,7 +43,7 @@ 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.5.59 * @version 1.5.60
* *
*/ */

عرض الملف

@@ -1,66 +1,96 @@
'use strict'; 'use strict';
const xss = require('xss'); const { JSDOM } = require('jsdom');
const DOMPurify = require('dompurify');
const he = require('he');
// Initialize DOMPurify with jsdom
const window = new JSDOM('').window;
const purify = DOMPurify(window);
const Logger = require('./Logger'); const Logger = require('./Logger');
const log = new Logger('Xss'); 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'], // 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) => { const checkXSS = (dataObject) => {
try { try {
if (Array.isArray(dataObject)) { return sanitizeData(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;
} catch (error) { } catch (error) {
log.error('XSS error', { data: dataObject, error: error }); log.error('Sanitization error:', error);
return dataObject; return dataObject; // Return original data in case of error
} }
}; };
function objectToJSONString(dataObject) { // Recursively sanitize data based on its type
try { function sanitizeData(data) {
return JSON.stringify(dataObject); if (typeof data === 'string') {
} catch (error) { // Decode HTML entities and URL encoded content
return false; const decodedData = he.decode(decodeURIComponent(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
} }
function JSONStringToObject(jsonString) { // Sanitize object properties
try { function sanitizeObject(obj) {
return JSON.parse(jsonString); return Object.keys(obj).reduce((acc, key) => {
} catch (error) { acc[key] = sanitizeData(obj[key]);
return false; return acc;
} }, {});
} }
module.exports = checkXSS; module.exports = checkXSS;

عرض الملف

@@ -1,6 +1,6 @@
{ {
"name": "mirotalksfu", "name": "mirotalksfu",
"version": "1.5.59", "version": "1.5.60",
"description": "WebRTC SFU browser-based video calls", "description": "WebRTC SFU browser-based video calls",
"main": "Server.js", "main": "Server.js",
"scripts": { "scripts": {
@@ -27,7 +27,8 @@
"nms-start": "docker-compose -f rtmpServers/node-media-server/docker-compose.yml up -d", "nms-start": "docker-compose -f rtmpServers/node-media-server/docker-compose.yml up -d",
"nms-stop": "docker-compose -f rtmpServers/node-media-server/docker-compose.yml down", "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-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" "nms-logs": "docker logs -f mirotalk-nms",
"unit-test": "npx mocha tests/checkXSS.js"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@@ -64,11 +65,14 @@
"compression": "1.7.4", "compression": "1.7.4",
"cors": "2.8.5", "cors": "2.8.5",
"crypto-js": "4.2.0", "crypto-js": "4.2.0",
"dompurify": "^3.1.6",
"express": "4.19.2", "express": "4.19.2",
"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",
"httpolyglot": "0.1.2", "httpolyglot": "0.1.2",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"jsdom": "^24.1.1",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"mediasoup": "3.14.11", "mediasoup": "3.14.11",
"mediasoup-client": "3.7.16", "mediasoup-client": "3.7.16",
@@ -78,12 +82,13 @@
"qs": "6.13.0", "qs": "6.13.0",
"socket.io": "4.7.5", "socket.io": "4.7.5",
"swagger-ui-express": "5.0.1", "swagger-ui-express": "5.0.1",
"uuid": "10.0.0", "uuid": "10.0.0"
"xss": "^1.0.15"
}, },
"devDependencies": { "devDependencies": {
"mocha": "^10.7.3",
"node-fetch": "^3.3.2", "node-fetch": "^3.3.2",
"nodemon": "^3.1.4", "nodemon": "^3.1.4",
"prettier": "3.3.3" "prettier": "3.3.3",
"should": "^13.2.3"
} }
} }

عرض الملف

@@ -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.5.59 * @version 1.5.60
* *
*/ */
@@ -4444,7 +4444,7 @@ function showAbout() {
imageUrl: image.about, imageUrl: image.about,
customClass: { image: 'img-about' }, customClass: { image: 'img-about' },
position: 'center', position: 'center',
title: 'WebRTC SFU v1.5.59', title: 'WebRTC SFU v1.5.60',
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.5.59 * @version 1.5.60
* *
*/ */

218
tests/checkXSS.js Normal file
عرض الملف

@@ -0,0 +1,218 @@
'use strict';
// npx mocha test-checkXSS.js
require('should');
const checkXSS = require('../app/src/XSS');
describe('checkXSS', () => {
// 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));
});
// 2. Simple String Handling
it('should sanitize strings with XSS injections', () => {
const maliciousString = '<script>alert("xss")</script>';
const sanitizedString = checkXSS(maliciousString);
sanitizedString.should.not.containEql('<script>');
sanitizedString.should.not.containEql('alert');
sanitizedString.should.not.containEql('</script>');
});
it('should sanitize complex XSS injections', () => {
const complexString = '<svg><g/onload=alert(2)//<p>';
const sanitizedString = checkXSS(complexString);
sanitizedString.should.not.containEql('onload');
sanitizedString.should.equal('<svg><g></g></svg>');
});
it('should sanitize HTML attributes', () => {
const maliciousHtml = '<a href="javascript:alert(\'xss\')">click me</a>';
const sanitizedHtml = checkXSS(maliciousHtml);
sanitizedHtml.should.not.containEql('javascript:');
sanitizedHtml.should.containEql('<a>click me</a>');
});
it('should sanitize embedded scripts in HTML', () => {
const maliciousHtml = '<div><script>alert("xss")</script></div>';
const sanitizedHtml = checkXSS(maliciousHtml);
sanitizedHtml.should.not.containEql('<script>');
sanitizedHtml.should.containEql('<div></div>');
});
it('should handle encoded XSS payloads', () => {
const encodedXss = '%3Cscript%3Ealert%28%27xss%27%29%3C%2Fscript%3E';
const sanitizedString = checkXSS(encodedXss);
sanitizedString.should.not.containEql('alert');
sanitizedString.should.equal('');
});
it('should handle special characters used in XSS attacks', () => {
const specialCharsXss = "<div title=\"<img src='x' onerror='alert(1)'/>\">Test</div>";
const sanitizedSpecialChars = checkXSS(specialCharsXss);
sanitizedSpecialChars.should.not.containEql('onerror');
sanitizedSpecialChars.should.containEql('<div>Test</div>');
});
// 3. Handling Objects, Arrays, and Nested Structures
it('should sanitize objects with XSS injections', () => {
const maliciousObject = {
key1: '<script>alert("xss")</script>',
key2: 'normal string',
};
const sanitizedObject = checkXSS(maliciousObject);
sanitizedObject.key1.should.not.containEql('<script>');
sanitizedObject.key1.should.not.containEql('alert');
sanitizedObject.key1.should.not.containEql('</script>');
sanitizedObject.key2.should.equal('normal string');
});
it('should sanitize arrays with XSS injections', () => {
const maliciousArray = ['<script>alert("xss")</script>', 'normal string'];
const sanitizedArray = checkXSS(maliciousArray);
sanitizedArray[0].should.not.containEql('<script>');
sanitizedArray[0].should.not.containEql('alert');
sanitizedArray[0].should.not.containEql('</script>');
sanitizedArray[1].should.equal('normal string');
});
it('should handle nested objects and arrays with XSS injections', () => {
const nestedData = {
key1: [
'<script>alert("xss")</script>',
{
key2: '<img src="x" onerror="alert(\'xss\')">',
},
],
};
const sanitizedData = checkXSS(nestedData);
sanitizedData.key1[0].should.not.containEql('<script>');
sanitizedData.key1[0].should.not.containEql('alert');
sanitizedData.key1[0].should.not.containEql('</script>');
sanitizedData.key1[1].key2.should.not.containEql('onerror');
sanitizedData.key1[1].key2.should.equal('<img src="x">');
});
it('should handle XSS in nested HTML elements', () => {
const nestedXss = '<div><span onclick="alert(\'xss\')">Click me</span></div>';
const sanitizedNestedXss = checkXSS(nestedXss);
sanitizedNestedXss.should.not.containEql('onclick');
sanitizedNestedXss.should.containEql('<div><span>Click me</span></div>');
});
it('should handle XSS through malicious attributes in different tags', () => {
const maliciousAttributes =
'<a href="#" onclick="alert(\'xss\')">Link</a><iframe src="javascript:alert(\'xss\')"></iframe>';
const sanitizedAttributes = checkXSS(maliciousAttributes);
sanitizedAttributes.should.not.containEql('onclick');
sanitizedAttributes.should.not.containEql('javascript:');
sanitizedAttributes.should.not.containEql('alert');
});
// 4. Handling Specific Formats (JSON, Base64, etc.)
it('should handle XSS in JSON data', () => {
const maliciousJson = '{"key": "<img src=\'x\' onerror=\'alert(1)\'>"}';
const sanitizedJson = checkXSS(JSON.parse(maliciousJson));
sanitizedJson.key.should.not.containEql('onerror');
sanitizedJson.key.should.equal('<img src="x">');
});
it('should sanitize base64 encoded content', () => {
const maliciousBase64 = '<img src="data:image/svg+xml;base64,PHN2ZyBvbmxvYWQ9YWxlcnQoJ3hzcicpPg==">';
const sanitizedBase64 = checkXSS(maliciousBase64);
sanitizedBase64.should.not.containEql('onload');
sanitizedBase64.should.equal('<img>');
});
it('should sanitize encoded HTML entities', () => {
const encodedHtmlEntities = '&lt;script&gt;alert(&#x27;xss&#x27;)&lt;/script&gt;';
const sanitizedEntities = checkXSS(encodedHtmlEntities);
sanitizedEntities.should.not.containEql('<script>');
sanitizedEntities.should.equal('');
});
it('should sanitize encoded and obfuscated payloads', () => {
const obfuscatedXss = '<img src="x" onerror="eval(String.fromCharCode(97,108,101,114,116) + \'(1)\')">';
const sanitizedObfuscatedXss = checkXSS(obfuscatedXss);
sanitizedObfuscatedXss.should.not.containEql('eval');
sanitizedObfuscatedXss.should.equal('<img src="x">');
});
// 5. Handling CSS and JavaScript URL XSS
it('should sanitize JavaScript and CSS injections', () => {
const jsInjection = '<div style="background-image: url(javascript:alert(\'xss\'))"></div>';
const sanitizedJsInjection = checkXSS(jsInjection);
sanitizedJsInjection.should.not.containEql('javascript:');
sanitizedJsInjection.should.containEql('<div></div>');
});
it('should handle JavaScript URL XSS', () => {
const jsUrlXss = '<a href="javascript:alert(\'xss\')">Click me</a>';
const sanitizedJsUrl = checkXSS(jsUrlXss);
sanitizedJsUrl.should.not.containEql('javascript:');
sanitizedJsUrl.should.containEql('<a>Click me</a>');
});
it('should sanitize `javascript:` URLs in CSS attributes', () => {
const maliciousCss = '<div style="background:url(javascript:alert(\'xss\'))"></div>';
const sanitizedCss = checkXSS(maliciousCss);
sanitizedCss.should.not.containEql('javascript:');
sanitizedCss.should.equal('<div></div>');
});
// 6. Handling SVG, MathML, and Data URIs
it('should handle XSS in SVG and MathML', () => {
const svgXss = '<svg><script>alert("xss")</script></svg>';
const sanitizedSvgXss = checkXSS(svgXss);
sanitizedSvgXss.should.not.containEql('<script>');
sanitizedSvgXss.should.equal('<svg></svg>');
});
it('should sanitize data URIs in HTML attributes', () => {
const maliciousHtml = '<img src="data:image/svg+xml,<svg onload=alert(\'xss\')>">';
const sanitizedHtml = checkXSS(maliciousHtml);
sanitizedHtml.should.not.containEql('onload');
sanitizedHtml.should.equal('<img>');
});
it('should handle data URL XSS', () => {
const dataUrlXss =
'<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNAAAAABJRU5ErkJggg==">';
const sanitizedDataUrl = checkXSS(dataUrlXss);
sanitizedDataUrl.should.not.containEql('data:');
sanitizedDataUrl.should.containEql('<img>');
});
// 7. Handling Dynamic Content
it('should handle XSS in dynamic content', () => {
const dynamicXss =
'<div id="dynamicContent"></div><script>document.getElementById("dynamicContent").innerHTML = "<img src=\'x\' onerror=\'alert(1)\'>" </script>';
const sanitizedDynamicXss = checkXSS(dynamicXss);
sanitizedDynamicXss.should.not.containEql('onerror');
sanitizedDynamicXss.should.containEql('<div id="dynamicContent"></div>');
});
// 8. Handling Mixed Content
it('should sanitize mixed content', () => {
const mixedContent = '<div>Normal text <script>alert("xss")</script> more text</div>';
const sanitizedContent = checkXSS(mixedContent);
sanitizedContent.should.not.containEql('<script>');
sanitizedContent.should.containEql('<div>Normal text more text</div>');
});
});