Skip to content

Commit e14fc33

Browse files
[mirotalk] - fix(server): prevent presenter role spoofing via display-name match
1 parent a89e3b5 commit e14fc33

7 files changed

Lines changed: 74 additions & 43 deletions

File tree

.env.template

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# ====================================================
2-
# MiroTalk P2P v.1.8.39 - Environment Configuration
2+
# MiroTalk P2P v.1.8.40 - Environment Configuration
33
# ====================================================
44

55
# App environment

app/src/config.template.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
/**
44
* ==============================================
5-
* MiroTalk P2P v.1.8.39 - Configuration File
5+
* MiroTalk P2P v.1.8.40 - Configuration File
66
* ==============================================
77
*
88
* This file is the central configuration source.

app/src/server.js

Lines changed: 56 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ dependencies: {
4545
* @license For commercial use or closed source, contact us at license.mirotalk@gmail.com or purchase directly from CodeCanyon
4646
* @license CodeCanyon: https://codecanyon.net/item/mirotalk-p2p-webrtc-realtime-video-conferences/38376661
4747
* @author Miroslav Pejic - miroslav.pejic.85@gmail.com
48-
* @version 1.8.39
48+
* @version 1.8.40
4949
*
5050
*/
5151

@@ -1436,6 +1436,29 @@ io.sockets.on('connect', async (socket) => {
14361436
peer_uuid: peer_uuid,
14371437
is_presenter: is_presenter,
14381438
};
1439+
1440+
// Recover presenter status on reconnect: if a stale presenter entry
1441+
// exists for this user (same peer_name AND same peer_uuid) under a
1442+
// previous socket.id, migrate it to the current socket.id. peer_uuid
1443+
// is never broadcast to other peers, so it cannot be spoofed by
1444+
// someone who only learned the display name.
1445+
for (const [existingPeerID, existingPresenter] of Object.entries(presenters[channel])) {
1446+
if (
1447+
existingPeerID !== socket.id &&
1448+
existingPresenter &&
1449+
existingPresenter.peer_name === peer_name &&
1450+
existingPresenter.peer_uuid === peer_uuid
1451+
) {
1452+
delete presenters[channel][existingPeerID];
1453+
presenters[channel][socket.id] = presenter;
1454+
log.debug('[' + socket.id + '] Presenter recovered on reconnect', {
1455+
previous_peer_id: existingPeerID,
1456+
peer_name: peer_name,
1457+
});
1458+
break;
1459+
}
1460+
}
1461+
14391462
// first we check if the username match the presenters username
14401463
if (roomPresenters && roomPresenters.includes(peer_name)) {
14411464
presenters[channel][socket.id] = presenter;
@@ -1572,15 +1595,16 @@ io.sockets.on('connect', async (socket) => {
15721595
}
15731596

15741597
//log.debug('[' + socket.id + '] Room action:', config);
1575-
const { room_id, peer_id, peer_name, peer_uuid, password, action } = config;
1598+
const { room_id, peer_name, peer_uuid, password, action } = config;
15761599

15771600
if (!peers[room_id]) {
15781601
log.warn('Room action room not found', { peer_id: socket.id, room_id });
15791602
return;
15801603
}
15811604

1582-
// Check if peer is presenter
1583-
const isPresenter = isPeerPresenter(room_id, peer_id, peer_name, peer_uuid);
1605+
// Check if peer is presenter using the server-controlled socket.id
1606+
// (NOT the client-supplied peer_id) to prevent role spoofing.
1607+
const isPresenter = isPeerPresenter(room_id, socket.id, peer_name, peer_uuid);
15841608

15851609
let room_is_locked = false;
15861610
//
@@ -1692,15 +1716,16 @@ io.sockets.on('connect', async (socket) => {
16921716

16931717
const { action, send_to_all, data } = config;
16941718

1695-
const { room_id, peer_id, peer_name, peer_uuid, to_peer_id } = data;
1719+
const { room_id, peer_name, peer_uuid, to_peer_id } = data;
16961720

16971721
log.debug('cmd', config);
16981722

16991723
// Only the presenter can do this actions
17001724
const presenterActions = ['geoLocation'];
17011725
if (presenterActions.some((v) => action === v)) {
1702-
// Check if peer is presenter
1703-
const isPresenter = isPeerPresenter(room_id, peer_id, peer_name, peer_uuid);
1726+
// Authorize using the server-controlled socket.id, not the
1727+
// client-supplied peer_id, to prevent role spoofing.
1728+
const isPresenter = isPeerPresenter(room_id, socket.id, peer_name, peer_uuid);
17041729
// if not presenter do nothing
17051730
if (!isPresenter) return;
17061731
}
@@ -1798,8 +1823,9 @@ io.sockets.on('connect', async (socket) => {
17981823
// Only the presenter can do this actions
17991824
const presenterActions = ['muteAudio', 'hideVideo', 'ejectAll'];
18001825
if (presenterActions.some((v) => peer_action === v)) {
1801-
// Check if peer is presenter
1802-
const isPresenter = isPeerPresenter(room_id, peer_id, peer_name, peer_uuid);
1826+
// Authorize using the server-controlled socket.id, not the
1827+
// client-supplied peer_id, to prevent role spoofing.
1828+
const isPresenter = isPeerPresenter(room_id, socket.id, peer_name, peer_uuid);
18031829
// if not presenter do nothing
18041830
if (!isPresenter) return;
18051831
}
@@ -1845,10 +1871,12 @@ io.sockets.on('connect', async (socket) => {
18451871

18461872
if (!Validate.isValidData(config)) return;
18471873

1874+
// peer_id here is the TARGET to kick; the caller's identity is the
1875+
// server-controlled socket.id, not anything the client supplies.
18481876
const { room_id, peer_id, peer_uuid, peer_name } = config;
18491877

1850-
// Check if peer is presenter
1851-
const isPresenter = await isPeerPresenter(room_id, peer_id, peer_name, peer_uuid);
1878+
// Authorize the caller using socket.id to prevent role spoofing.
1879+
const isPresenter = isPeerPresenter(room_id, socket.id, peer_name, peer_uuid);
18521880

18531881
// Only the presenter can kickOut others
18541882
if (isPresenter) {
@@ -2160,25 +2188,28 @@ async function getPeerGeoLocation(ip) {
21602188
*/
21612189
function isPeerPresenter(room_id, peer_id, peer_name, peer_uuid) {
21622190
try {
2163-
if (!presenters[room_id] || !presenters[room_id][peer_id]) {
2164-
// Presenter not in the presenters config list, disconnected, or peer_id changed...
2165-
for (const [existingPeerID, presenter] of Object.entries(presenters[room_id] || {})) {
2166-
if (presenter.peer_name === peer_name) {
2167-
log.debug('[' + peer_id + '] Presenter found', presenters[room_id][existingPeerID]);
2168-
return true;
2169-
}
2170-
}
2191+
// peer_id MUST be the server-controlled socket.id of the caller.
2192+
// Never trust a client-supplied peer_id, peer_name, or peer_uuid for
2193+
// authorization decisions: look up the stored presenter entry by
2194+
// socket.id and require both peer_name and peer_uuid to match what
2195+
// was recorded at join time.
2196+
const roomPresentersMap = presenters[room_id];
2197+
const stored = roomPresentersMap && roomPresentersMap[peer_id];
2198+
2199+
if (!stored) {
2200+
log.debug('[' + peer_id + '] isPeerPresenter - no stored presenter entry for caller', {
2201+
peer_name: peer_name,
2202+
});
21712203
return false;
21722204
}
21732205

21742206
const isPresenter =
2175-
(typeof presenters[room_id] === 'object' &&
2176-
Object.keys(presenters[room_id][peer_id]).length > 1 &&
2177-
presenters[room_id][peer_id]['peer_name'] === peer_name &&
2178-
presenters[room_id][peer_id]['peer_uuid'] === peer_uuid) ||
2179-
(roomPresenters && roomPresenters.includes(peer_name));
2207+
typeof stored === 'object' &&
2208+
Object.keys(stored).length > 1 &&
2209+
stored.peer_name === peer_name &&
2210+
stored.peer_uuid === peer_uuid;
21802211

2181-
log.debug('[' + peer_id + '] isPeerPresenter', presenters[room_id][peer_id]);
2212+
log.debug('[' + peer_id + '] isPeerPresenter', { stored, isPresenter });
21822213

21832214
return isPresenter;
21842215
} catch (err) {

package-lock.json

Lines changed: 10 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "mirotalk",
3-
"version": "1.8.39",
3+
"version": "1.8.40",
44
"description": "A free WebRTC browser-based video call",
55
"main": "server.js",
66
"scripts": {
@@ -53,7 +53,7 @@
5353
"compression": "^1.8.1",
5454
"cors": "^2.8.6",
5555
"crypto-js": "^4.2.0",
56-
"dompurify": "^3.4.3",
56+
"dompurify": "^3.4.4",
5757
"dotenv": "^17.4.2",
5858
"express": "^5.2.1",
5959
"express-openid-connect": "^3.0.0",
@@ -66,7 +66,7 @@
6666
"jsonwebtoken": "^9.0.3",
6767
"nodemailer": "^8.0.7",
6868
"openai": "^6.38.0",
69-
"qs": "^6.15.1",
69+
"qs": "^6.15.2",
7070
"socket.io": "^4.8.3",
7171
"swagger-ui-express": "^5.0.1",
7272
"uuid": "14.0.0"

public/js/brand.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ let brand = {
109109
},
110110
about: {
111111
imageUrl: '../images/mirotalk-logo.gif',
112-
title: 'WebRTC P2P v1.8.39',
112+
title: 'WebRTC P2P v1.8.40',
113113
html: `
114114
<button
115115
id="support-button"

public/js/client.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
* @license For commercial use or closed source, contact us at license.mirotalk@gmail.com or purchase directly from CodeCanyon
1616
* @license CodeCanyon: https://codecanyon.net/item/mirotalk-p2p-webrtc-realtime-video-conferences/38376661
1717
* @author Miroslav Pejic - miroslav.pejic.85@gmail.com
18-
* @version 1.8.39
18+
* @version 1.8.40
1919
*
2020
*/
2121

@@ -15823,7 +15823,7 @@ function showAbout() {
1582315823
Swal.fire({
1582415824
background: swBg,
1582515825
position: 'center',
15826-
title: brand.about?.title && brand.about.title.trim() !== '' ? brand.about.title : 'WebRTC P2P v1.8.39',
15826+
title: brand.about?.title && brand.about.title.trim() !== '' ? brand.about.title : 'WebRTC P2P v1.8.40',
1582715827
imageUrl: brand.about?.imageUrl && brand.about.imageUrl.trim() !== '' ? brand.about.imageUrl : images.about,
1582815828
customClass: { image: 'img-about' },
1582915829
html: renderRoomTemplate('tpl-about-modal', {

0 commit comments

Comments
 (0)