(y-provider) check hocuspocus documentName validity

We only use uuid v4 as hocuspocus dicument name. To be sure nothing else
is used we check that the documentName is a valid uuid version 4.
This commit is contained in:
Manuel Raynaud
2025-03-27 15:50:27 +01:00
parent 8bee476b5b
commit 7e1eed3abd
4 changed files with 134 additions and 40 deletions

View File

@@ -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();
});
},
});

View File

@@ -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": "*"
},

View File

@@ -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'));
}

View File

@@ -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==