Skip to content

Commit a24e803

Browse files
authored
feat: implement message size validation to prevent excessive payloads (#1197)
* feat: introduce message size validation Added MAX_MESSAGE_LENGTH constant and implemented validation to ensure messages do not exceed the maximum allowed size in MobilePortStream and RemoteCommunicationPostMessageStream. * feat: adding unit tests * feat: linting * feat: unit tests * feat: cleanup * feat: add size validation ot socket server
1 parent 458c3fa commit a24e803

File tree

7 files changed

+181
-14
lines changed

7 files changed

+181
-14
lines changed

packages/sdk-socket-server-next/src/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export const withAdminUI: boolean = process.env.ADMIN_UI === 'true';
1717
const HOUR_IN_SECONDS = 60 * 60;
1818
const THIRTY_DAYS_IN_SECONDS = 30 * 24 * 60 * 60; // expiration time of entries in Redis
1919
export const MAX_CLIENTS_PER_ROOM = 2;
20+
export const MAX_MESSAGE_LENGTH = 1_000_000; // 1MB limit
2021

2122
export const config = {
2223
msgExpiry: HOUR_IN_SECONDS,

packages/sdk-socket-server-next/src/protocol/handleMessage.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Server, Socket } from 'socket.io';
22
import { v4 as uuidv4 } from 'uuid';
33
import { pubClient } from '../analytics-api';
4-
import { config, isDevelopment } from '../config';
4+
import { config, isDevelopment, MAX_MESSAGE_LENGTH } from '../config';
55
import { getLogger } from '../logger';
66
import {
77
increaseRateLimits,
@@ -58,6 +58,21 @@ export const handleMessage = async ({
5858
let ready = false; // Determines if the keys have been exchanged and both side support the full protocol
5959

6060
try {
61+
// Add message size validation
62+
const messageSize = typeof message === 'string'
63+
? message.length
64+
: JSON.stringify(message).length;
65+
66+
if (messageSize > MAX_MESSAGE_LENGTH) {
67+
logger.warn(`[handleMessage] Message size ${messageSize} exceeds limit of ${MAX_MESSAGE_LENGTH} bytes`, {
68+
channelId,
69+
socketId,
70+
clientIp,
71+
});
72+
callback?.(`Message size ${messageSize} exceeds maximum allowed size of ${MAX_MESSAGE_LENGTH} bytes`);
73+
return;
74+
}
75+
6176
if (clientType) {
6277
// new protocol, get channelConfig
6378
const channelConfigKey = `channel_config:${channelId}`;

packages/sdk/src/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,5 @@ export const EXTENSION_EVENTS = {
7373
CONNECT: 'connect',
7474
CONNECTED: 'connected',
7575
};
76+
77+
export const MAX_MESSAGE_LENGTH = 1_000_000; // 1MB limit

packages/sdk/src/services/MobilePortStream/write.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Buffer } from 'buffer';
2+
import { MAX_MESSAGE_LENGTH } from '../../config';
23
import { write } from './write';
34

45
describe('write function', () => {
@@ -77,4 +78,50 @@ describe('write function', () => {
7778
new Error('MobilePortStream - disconnected'),
7879
);
7980
});
81+
82+
describe('Message Size Validation', () => {
83+
beforeEach(() => {
84+
jest.clearAllMocks();
85+
global.window = {
86+
location: { href: 'http://example.com' },
87+
ReactNativeWebView: { postMessage: mockPostMessage },
88+
} as any;
89+
});
90+
91+
it('should reject messages exceeding MAX_MESSAGE_LENGTH', () => {
92+
const largeData = {
93+
data: {
94+
jsonrpc: '2.0',
95+
method: 'test_method',
96+
params: ['x'.repeat(MAX_MESSAGE_LENGTH)],
97+
},
98+
};
99+
100+
write(largeData, 'utf-8', cb);
101+
102+
expect(cb).toHaveBeenCalledWith(
103+
expect.objectContaining({
104+
message: expect.stringMatching(
105+
/Message size \d+ exceeds maximum allowed size of \d+ bytes/u,
106+
),
107+
}),
108+
);
109+
expect(mockPostMessage).not.toHaveBeenCalled();
110+
});
111+
112+
it('should accept messages within MAX_MESSAGE_LENGTH', () => {
113+
const validData = {
114+
data: {
115+
jsonrpc: '2.0',
116+
method: 'test_method',
117+
params: ['x'.repeat(100)],
118+
},
119+
};
120+
121+
write(validData, 'utf-8', cb);
122+
123+
expect(cb).toHaveBeenCalledWith();
124+
expect(mockPostMessage).toHaveBeenCalled();
125+
});
126+
});
80127
});

packages/sdk/src/services/MobilePortStream/write.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Buffer } from 'buffer';
2+
import { MAX_MESSAGE_LENGTH } from '../../config';
23

34
/**
45
* Handles communication between the in-app browser and MetaMask mobile application.
@@ -15,6 +16,7 @@ export function write(
1516
cb: (error?: Error | null) => void,
1617
) {
1718
try {
19+
let stringifiedData: string;
1820
if (Buffer.isBuffer(chunk)) {
1921
const data: {
2022
type: 'Buffer';
@@ -23,18 +25,30 @@ export function write(
2325
} = chunk.toJSON();
2426

2527
data._isBuffer = true;
26-
window.ReactNativeWebView?.postMessage(
27-
JSON.stringify({ ...data, origin: window.location.href }),
28-
);
28+
stringifiedData = JSON.stringify({
29+
...data,
30+
origin: window.location.href,
31+
});
2932
} else {
3033
if (chunk.data) {
3134
chunk.data.toNative = true;
3235
}
3336

34-
window.ReactNativeWebView?.postMessage(
35-
JSON.stringify({ ...chunk, origin: window.location.href }),
37+
stringifiedData = JSON.stringify({
38+
...chunk,
39+
origin: window.location.href,
40+
});
41+
}
42+
43+
if (stringifiedData.length > MAX_MESSAGE_LENGTH) {
44+
return cb(
45+
new Error(
46+
`Message size ${stringifiedData.length} exceeds maximum allowed size of ${MAX_MESSAGE_LENGTH} bytes`,
47+
),
3648
);
3749
}
50+
51+
window.ReactNativeWebView?.postMessage(stringifiedData);
3852
} catch (err) {
3953
return cb(new Error('MobilePortStream - disconnected'));
4054
}

packages/sdk/src/services/RemoteCommunicationPostMessageStream/write.test.ts

Lines changed: 81 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { Ethereum } from '../Ethereum'; // Adjust the import based on your project structure
21
import { RemoteCommunicationPostMessageStream } from '../../PostMessageStream/RemoteCommunicationPostMessageStream'; // Adjust the import based on your project structure
3-
import { METHODS_TO_REDIRECT } from '../../config';
2+
import { MAX_MESSAGE_LENGTH, METHODS_TO_REDIRECT } from '../../config';
43
import * as loggerModule from '../../utils/logger'; // Adjust the import based on your project structure
5-
import { write } from './write'; // Adjust the import based on your project structure
4+
import { Ethereum } from '../Ethereum'; // Adjust the import based on your project structure
65
import { extractMethod } from './extractMethod';
6+
import { write } from './write'; // Adjust the import based on your project structure
77

88
jest.mock('./extractMethod');
99
jest.mock('../Ethereum');
@@ -162,11 +162,22 @@ describe('write function', () => {
162162
mockIsMobileWeb.mockReturnValue(false);
163163
mockIsSecure.mockReturnValue(true);
164164
mockGetChannelId.mockReturnValue('some_channel_id');
165+
mockIsMetaMaskInstalled.mockReturnValue(true);
166+
mockGetKeyInfo.mockReturnValue({ ecies: { public: 'test_public_key' } });
167+
mockHasDeeplinkProtocol.mockReturnValue(false);
165168
});
166169

167170
it('should redirect if method exists in METHODS_TO_REDIRECT', async () => {
168171
mockExtractMethod.mockReturnValue({
169172
method: Object.keys(METHODS_TO_REDIRECT)[0],
173+
data: {
174+
data: {
175+
jsonrpc: '2.0',
176+
method: Object.keys(METHODS_TO_REDIRECT)[0],
177+
params: [],
178+
},
179+
},
180+
triggeredInstaller: false,
170181
});
171182

172183
await write(
@@ -239,4 +250,71 @@ describe('write function', () => {
239250
expect(spyLogger).toHaveBeenCalled();
240251
});
241252
});
253+
254+
describe('Message Size Validation', () => {
255+
it('should reject messages exceeding MAX_MESSAGE_LENGTH', async () => {
256+
mockGetChannelId.mockReturnValue('some_channel_id');
257+
mockIsReady.mockReturnValue(true);
258+
mockIsConnected.mockReturnValue(true);
259+
260+
// Mock extractMethod to return large data
261+
const largeData = {
262+
jsonrpc: '2.0',
263+
method: 'eth_call',
264+
params: ['x'.repeat(MAX_MESSAGE_LENGTH + 1)],
265+
};
266+
267+
mockExtractMethod.mockReturnValue({
268+
method: 'eth_call',
269+
data: {
270+
data: largeData,
271+
},
272+
});
273+
274+
await write(
275+
mockRemoteCommunicationPostMessageStream,
276+
{ jsonrpc: '2.0', method: 'eth_call' },
277+
'utf8',
278+
callback,
279+
);
280+
281+
// Don't test for exact error message, just verify it contains the key parts
282+
expect(callback).toHaveBeenCalledWith(
283+
expect.objectContaining({
284+
message: expect.stringMatching(
285+
/Message size \d+ exceeds maximum allowed size of \d+ bytes/u,
286+
),
287+
}),
288+
);
289+
expect(mockSendMessage).not.toHaveBeenCalled();
290+
});
291+
292+
it('should accept messages within MAX_MESSAGE_LENGTH', async () => {
293+
mockGetChannelId.mockReturnValue('some_channel_id');
294+
mockIsReady.mockReturnValue(true);
295+
mockIsConnected.mockReturnValue(true);
296+
297+
// Mock extractMethod to return valid-sized data
298+
mockExtractMethod.mockReturnValue({
299+
method: 'eth_call',
300+
data: {
301+
data: {
302+
jsonrpc: '2.0',
303+
method: 'eth_call',
304+
params: ['x'.repeat(100)],
305+
},
306+
},
307+
});
308+
309+
await write(
310+
mockRemoteCommunicationPostMessageStream,
311+
{ jsonrpc: '2.0', method: 'eth_call' },
312+
'utf8',
313+
callback,
314+
);
315+
316+
expect(callback).toHaveBeenCalledWith();
317+
expect(mockSendMessage).toHaveBeenCalled();
318+
});
319+
});
242320
});

packages/sdk/src/services/RemoteCommunicationPostMessageStream/write.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { RemoteCommunicationPostMessageStream } from '../../PostMessageStream/RemoteCommunicationPostMessageStream';
2-
import { METHODS_TO_REDIRECT, RPC_METHODS } from '../../config';
2+
import {
3+
METHODS_TO_REDIRECT,
4+
RPC_METHODS,
5+
MAX_MESSAGE_LENGTH,
6+
} from '../../config';
37
import {
48
METAMASK_CONNECT_BASE_URL,
59
METAMASK_DEEPLINK_BASE,
@@ -57,11 +61,17 @@ export async function write(
5761
deeplinkProtocolAvailable && mobileWeb && authorized;
5862

5963
try {
60-
console.warn(
61-
`[RCPMS: _write()] triggeredInstaller=${triggeredInstaller} activeDeeplinkProtocol=${activeDeeplinkProtocol}`,
62-
);
63-
6464
if (!triggeredInstaller) {
65+
// Check message size before sending
66+
const stringifiedData = JSON.stringify(data?.data);
67+
if (stringifiedData.length > MAX_MESSAGE_LENGTH) {
68+
return callback(
69+
new Error(
70+
`Message size ${stringifiedData.length} exceeds maximum allowed size of ${MAX_MESSAGE_LENGTH} bytes`,
71+
),
72+
);
73+
}
74+
6575
// The only reason not to send via network is because the rpc call will be sent in the deeplink
6676
instance.state.remote
6777
?.sendMessage(data?.data)

0 commit comments

Comments
 (0)