diff --git a/src/frontend/servers/y-provider/__tests__/hocusPocusWS.test.ts b/src/frontend/servers/y-provider/__tests__/hocusPocusWS.test.ts index 1ee8f9ac..1b8006b7 100644 --- a/src/frontend/servers/y-provider/__tests__/hocusPocusWS.test.ts +++ b/src/frontend/servers/y-provider/__tests__/hocusPocusWS.test.ts @@ -2,6 +2,7 @@ import { HocuspocusProvider, HocuspocusProviderWebsocket, } from '@hocuspocus/provider'; +import { v1 as uuidv1, v4 as uuidv4 } from 'uuid'; import WebSocket from 'ws'; const port = 5559; @@ -52,9 +53,9 @@ describe('Server Tests', () => { test('WebSocket connection with bad origin should be closed', () => { const { promise, done } = promiseDone(); - + const room = uuidv4(); const ws = new WebSocket( - `ws://localhost:${port}/collaboration/ws/?room=test-room`, + `ws://localhost:${port}/collaboration/ws/?room=${room}`, { headers: { Origin: 'http://bad-origin.com', @@ -72,9 +73,9 @@ describe('Server Tests', () => { test('WebSocket connection without cookies header should be closed', () => { const { promise, done } = promiseDone(); - + const room = uuidv4(); const ws = new WebSocket( - `ws://localhost:${port}/collaboration/ws/?room=test-room`, + `ws://localhost:${port}/collaboration/ws/?room=${room}`, { headers: { Origin: origin, @@ -92,9 +93,46 @@ describe('Server Tests', () => { test('WebSocket connection not allowed if room not matching provider name', () => { const { promise, done } = promiseDone(); - + const room = uuidv4(); const wsHocus = new HocuspocusProviderWebsocket({ - url: `ws://localhost:${portWS}/?room=my-test`, + url: `ws://localhost:${portWS}/?room=${room}`, + WebSocketPolyfill: WebSocket, + maxAttempts: 1, + quiet: true, + }); + + const providerName = uuidv4(); + const provider = new HocuspocusProvider({ + websocketProvider: wsHocus, + name: providerName, + broadcast: false, + quiet: true, + preserveConnection: false, + onClose: (data) => { + expect(console.error).toHaveBeenCalledWith( + 'Invalid room name - Probable hacking attempt:', + providerName, + room, + ); + + wsHocus.stopConnectionAttempt(); + expect(data.event.reason).toBe('Forbidden'); + wsHocus.webSocket?.close(); + wsHocus.disconnect(); + provider.destroy(); + wsHocus.destroy(); + done(); + }, + }); + + return promise; + }); + + test('WebSocket connection not allowed if room is not a valid uuid v4', () => { + const { promise, done } = promiseDone(); + const room = uuidv1(); + const wsHocus = new HocuspocusProviderWebsocket({ + url: `ws://localhost:${portWS}/?room=${room}`, WebSocketPolyfill: WebSocket, maxAttempts: 1, quiet: true, @@ -102,15 +140,49 @@ describe('Server Tests', () => { const provider = new HocuspocusProvider({ websocketProvider: wsHocus, - name: 'hocuspocus-test', + name: room, broadcast: false, quiet: true, preserveConnection: false, onClose: (data) => { expect(console.error).toHaveBeenCalledWith( - 'Invalid room name - Probable hacking attempt:', - 'hocuspocus-test', - 'my-test', + 'Room name is not a valid uuid:', + room, + ); + + wsHocus.stopConnectionAttempt(); + expect(data.event.reason).toBe('Forbidden'); + wsHocus.webSocket?.close(); + wsHocus.disconnect(); + provider.destroy(); + wsHocus.destroy(); + done(); + }, + }); + + return promise; + }); + + test('WebSocket connection not allowed if room is not a valid uuid', () => { + const { promise, done } = promiseDone(); + const room = 'not-a-valid-uuid'; + const wsHocus = new HocuspocusProviderWebsocket({ + url: `ws://localhost:${portWS}/?room=${room}`, + WebSocketPolyfill: WebSocket, + maxAttempts: 1, + quiet: true, + }); + + const provider = new HocuspocusProvider({ + websocketProvider: wsHocus, + name: room, + broadcast: false, + quiet: true, + preserveConnection: false, + onClose: (data) => { + expect(console.error).toHaveBeenCalledWith( + 'Room name is not a valid uuid:', + room, ); wsHocus.stopConnectionAttempt(); @@ -131,8 +203,9 @@ describe('Server Tests', () => { mockDocFetch.mockRejectedValue(''); + const room = uuidv4(); const wsHocus = new HocuspocusProviderWebsocket({ - url: `ws://localhost:${portWS}/?room=my-test`, + url: `ws://localhost:${portWS}/?room=${room}`, WebSocketPolyfill: WebSocket, maxAttempts: 1, quiet: true, @@ -140,7 +213,7 @@ describe('Server Tests', () => { const provider = new HocuspocusProvider({ websocketProvider: wsHocus, - name: 'my-test', + name: room, broadcast: false, quiet: true, preserveConnection: false, @@ -167,6 +240,7 @@ describe('Server Tests', () => { test('WebSocket connection fails if user do not have correct retrieve ability', () => { const { promise, done } = promiseDone(); + const room = uuidv4(); mockDocFetch.mockResolvedValue({ abilities: { retrieve: false, @@ -174,7 +248,7 @@ describe('Server Tests', () => { }); const wsHocus = new HocuspocusProviderWebsocket({ - url: `ws://localhost:${portWS}/?room=my-test`, + url: `ws://localhost:${portWS}/?room=${room}`, WebSocketPolyfill: WebSocket, maxAttempts: 1, quiet: true, @@ -182,14 +256,14 @@ describe('Server Tests', () => { const provider = new HocuspocusProvider({ websocketProvider: wsHocus, - name: 'my-test', + name: room, broadcast: false, quiet: true, preserveConnection: false, onClose: (data) => { expect(console.error).toHaveBeenCalledWith( 'onConnect: Unauthorized to retrieve this document', - 'my-test', + room, ); wsHocus.stopConnectionAttempt(); @@ -217,19 +291,20 @@ describe('Server Tests', () => { }, }); + const room = uuidv4(); const wsHocus = new HocuspocusProviderWebsocket({ - url: `ws://localhost:${portWS}/?room=hocuspocus-test`, + url: `ws://localhost:${portWS}/?room=${room}`, WebSocketPolyfill: WebSocket, }); const provider = new HocuspocusProvider({ websocketProvider: wsHocus, - name: 'hocuspocus-test', + name: room, broadcast: false, quiet: true, onConnect: () => { void hocusPocusServer - .openDirectConnection('hocuspocus-test') + .openDirectConnection(room) .then((connection) => { connection.document?.getConnections().forEach((connection) => { expect(connection.readOnly).toBe(!canEdit); @@ -262,29 +337,28 @@ describe('Server Tests', () => { id: 'test-user-id', }); + const room = uuidv4(); const wsHocus = new HocuspocusProviderWebsocket({ - url: `ws://localhost:${portWS}/?room=hocuspocus-test`, + url: `ws://localhost:${portWS}/?room=${room}`, WebSocketPolyfill: WebSocket, }); const provider = new HocuspocusProvider({ websocketProvider: wsHocus, - name: 'hocuspocus-test', + name: room, broadcast: false, quiet: true, onConnect: () => { - void hocusPocusServer - .openDirectConnection('hocuspocus-test') - .then((connection) => { - connection.document?.getConnections().forEach((connection) => { - expect(connection.context.userId).toBe('test-user-id'); - }); - - void connection.disconnect(); - provider.destroy(); - wsHocus.destroy(); - done(); + void hocusPocusServer.openDirectConnection(room).then((connection) => { + connection.document?.getConnections().forEach((connection) => { + expect(connection.context.userId).toBe('test-user-id'); }); + + void connection.disconnect(); + provider.destroy(); + wsHocus.destroy(); + done(); + }); }, }); diff --git a/src/frontend/servers/y-provider/package.json b/src/frontend/servers/y-provider/package.json index 415726e0..5b8b56cb 100644 --- a/src/frontend/servers/y-provider/package.json +++ b/src/frontend/servers/y-provider/package.json @@ -24,6 +24,7 @@ "cors": "2.8.5", "express": "4.21.2", "express-ws": "5.0.2", + "uuid": "11.1.0", "y-protocols": "1.0.6", "yjs": "*" }, diff --git a/src/frontend/servers/y-provider/src/servers/hocusPocusServer.ts b/src/frontend/servers/y-provider/src/servers/hocusPocusServer.ts index 70c3eb2d..36f506ba 100644 --- a/src/frontend/servers/y-provider/src/servers/hocusPocusServer.ts +++ b/src/frontend/servers/y-provider/src/servers/hocusPocusServer.ts @@ -1,4 +1,5 @@ import { Server } from '@hocuspocus/server'; +import { validate as uuidValidate, version as uuidVersion } from 'uuid'; import { fetchDocument } from '@/api/getDoc'; import { getMe } from '@/api/getMe'; @@ -27,6 +28,12 @@ export const hocusPocusServer = Server.configure({ return Promise.reject(new Error('Wrong room name: Unauthorized')); } + if (!uuidValidate(documentName) || uuidVersion(documentName) !== 4) { + console.error('Room name is not a valid uuid:', documentName); + + return Promise.reject(new Error('Wrong room name: Unauthorized')); + } + let can_edit = false; try { @@ -35,7 +42,7 @@ export const hocusPocusServer = Server.configure({ if (!document.abilities.retrieve) { console.error( 'onConnect: Unauthorized to retrieve this document', - roomParam, + documentName, ); return Promise.reject(new Error('Wrong abilities:Unauthorized')); } diff --git a/src/frontend/yarn.lock b/src/frontend/yarn.lock index 5328c165..c78b8b75 100644 --- a/src/frontend/yarn.lock +++ b/src/frontend/yarn.lock @@ -6334,13 +6334,20 @@ dependencies: "@types/node" "*" -"@types/node@*", "@types/node@22.10.7", "@types/node@22.13.9", "@types/node@^22.7.5": +"@types/node@*", "@types/node@^22.7.5": version "22.13.9" resolved "https://registry.yarnpkg.com/@types/node/-/node-22.13.9.tgz#5d9a8f7a975a5bd3ef267352deb96fb13ec02eca" integrity sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw== dependencies: undici-types "~6.20.0" +"@types/node@22.10.7": + version "22.10.7" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.10.7.tgz#14a1ca33fd0ebdd9d63593ed8d3fbc882a6d28d7" + integrity sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg== + dependencies: + undici-types "~6.20.0" + "@types/parse-json@^4.0.0": version "4.0.2" resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.2.tgz#5950e50960793055845e956c427fc2b0d70c5239" @@ -6396,7 +6403,7 @@ resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb" integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== -"@types/react-dom@*", "@types/react-dom@19.0.0": +"@types/react-dom@*": version "19.0.0" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.0.0.tgz#e7f5d618a080486eaf9952246dbf59eaa2c64130" integrity sha512-1KfiQKsH1o00p9m5ag12axHQSb3FOU9H20UTrujVSkNhuCrRHiQWFqgEnTNK5ZNfnzZv8UWrnXVqCmCF9fgY3w== @@ -6415,7 +6422,7 @@ resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.12.tgz#b5d76568485b02a307238270bfe96cb51ee2a044" integrity sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w== -"@types/react@*", "@types/react@19.0.0": +"@types/react@*": version "19.0.0" resolved "https://registry.yarnpkg.com/@types/react/-/react-19.0.0.tgz#fbbb53ce223f4e2b750ad5dd09580b2c43522bbf" integrity sha512-MY3oPudxvMYyesqs/kW1Bh8y9VqSmf+tzqw3ae8a9DZW68pUe3zAdHeI1jc6iAysuRdACnVknHP8AhwD4/dxtg== @@ -6533,7 +6540,7 @@ dependencies: "@types/yargs-parser" "*" -"@typescript-eslint/eslint-plugin@*", "@typescript-eslint/eslint-plugin@8.26.0", "@typescript-eslint/eslint-plugin@^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0": +"@typescript-eslint/eslint-plugin@*", "@typescript-eslint/eslint-plugin@^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0": version "8.26.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.26.0.tgz#7e880faf91f89471c30c141951e15f0eb3a0599e" integrity sha512-cLr1J6pe56zjKYajK6SSSre6nl1Gj6xDp1TY0trpgPzjVbgDwd09v2Ws37LABxzkicmUjhEeg/fAUjPJJB1v5Q== @@ -6548,7 +6555,7 @@ natural-compare "^1.4.0" ts-api-utils "^2.0.1" -"@typescript-eslint/parser@*", "@typescript-eslint/parser@8.26.0", "@typescript-eslint/parser@^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0": +"@typescript-eslint/parser@*", "@typescript-eslint/parser@^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0": version "8.26.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.26.0.tgz#9b4d2198e89f64fb81e83167eedd89a827d843a9" integrity sha512-mNtXP9LTVBy14ZF3o7JG69gRPBK/2QWtQd0j0oH26HcY/foyJJau6pNUez7QrM5UHnSvwlQcJXKsk0I99B9pOA== @@ -8793,7 +8800,7 @@ eslint-visitor-keys@^4.2.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz#687bacb2af884fcdda8a6e7d65c606f46a14cd45" integrity sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw== -eslint@*, eslint@8.57.0: +eslint@*: version "8.57.0" resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.0.tgz#c786a6fd0e0b68941aaf624596fb987089195668" integrity sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ== @@ -15086,7 +15093,7 @@ typed-array-length@^1.0.7: possible-typed-array-names "^1.0.0" reflect.getprototypeof "^1.0.6" -typescript@*, typescript@5.8.2, typescript@^5.0.4: +typescript@*, typescript@^5.0.4: version "5.8.2" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.2.tgz#8170b3702f74b79db2e5a96207c15e65807999e4" integrity sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ== @@ -15387,6 +15394,11 @@ utils-merge@1.0.1: resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== +uuid@11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-11.1.0.tgz#9549028be1753bb934fc96e2bca09bb4105ae912" + integrity sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A== + uuid@^11.0.3: version "11.0.5" resolved "https://registry.yarnpkg.com/uuid/-/uuid-11.0.5.tgz#07b46bdfa6310c92c3fb3953a8720f170427fc62" @@ -16103,7 +16115,7 @@ yargs@17.7.2, yargs@^17.3.1: y18n "^5.0.5" yargs-parser "^21.1.1" -yjs@*, yjs@13.6.23, yjs@^13.6.15: +yjs@*, yjs@^13.6.15: version "13.6.23" resolved "https://registry.yarnpkg.com/yjs/-/yjs-13.6.23.tgz#62358dfa52e92dc870b8a0bedcf0d4cbd4c5ffa8" integrity sha512-ExtnT5WIOVpkL56bhLeisG/N5c4fmzKn4k0ROVfJa5TY2QHbH7F0Wu2T5ZhR7ErsFWQEFafyrnSI8TPKVF9Few==