Skip to content

Commit e24fff5

Browse files
committed
feat: BX-2109
1 parent 749c5bf commit e24fff5

File tree

3 files changed

+79
-1
lines changed

3 files changed

+79
-1
lines changed

src/core/messengers/initializeMessenger.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { ScriptType, detectScriptType } from '../utils/detectScriptType';
2+
import { isInternalOrigin } from '../utils/isInternalOrigin';
23

34
import { bridgeMessenger } from './internal/bridge';
5+
import { CallbackOptions, Messenger } from './internal/createMessenger';
46
import { extensionMessenger } from './internal/extension';
57
import { tabMessenger } from './internal/tab';
68
import { windowMessenger } from './internal/window';
@@ -19,6 +21,34 @@ type InitializeMessengerArgs = {
1921
connect: ScriptType;
2022
};
2123

24+
/**
25+
* Wraps a messenger to validate that all incoming messages originate from
26+
* extension URLs. This prevents cross-origin message handler bypass attacks
27+
* where malicious websites attempt to send messages to privileged handlers.
28+
*/
29+
function withOriginValidation(messenger: Messenger): Messenger {
30+
return {
31+
...messenger,
32+
reply<TPayload, TResponse>(
33+
topic: string,
34+
callback: (
35+
payload: TPayload,
36+
options: CallbackOptions,
37+
) => Promise<TResponse>,
38+
) {
39+
return messenger.reply<TPayload, TResponse>(
40+
topic,
41+
async (payload, options) => {
42+
if (!isInternalOrigin(options.sender, `messenger:${topic}`)) {
43+
return { error: 'Invalid origin' } as TResponse;
44+
}
45+
return callback(payload, options);
46+
},
47+
);
48+
},
49+
};
50+
}
51+
2252
export function initializeMessenger({ connect }: InitializeMessengerArgs) {
2353
const source = detectScriptType();
2454
const connections = [
@@ -31,5 +61,13 @@ export function initializeMessenger({ connect }: InitializeMessengerArgs) {
3161
`No messenger found for connection ${source} <-> ${connect}.`,
3262
);
3363

34-
return messengersForConnection[connection];
64+
const messenger = messengersForConnection[connection];
65+
66+
// When background expects messages from popup, enforce origin validation
67+
// to prevent cross-origin message handler bypass attacks
68+
if (source === 'background' && connect === 'popup') {
69+
return withOriginValidation(messenger);
70+
}
71+
72+
return messenger;
3573
}

src/core/utils/isInternalOrigin.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { IMessageSender } from '@rainbow-me/provider';
2+
3+
import { RainbowError, logger } from '~/logger';
4+
5+
/**
6+
* Validates that a message sender originates from within the extension.
7+
* Used to prevent cross-origin message handler bypass attacks where
8+
* malicious websites attempt to send messages to privileged handlers.
9+
*
10+
* @param sender - The message sender object containing URL and origin info
11+
* @param source - Identifier for logging purposes (e.g., 'messenger:wallet_action')
12+
* @returns true if the sender is from an internal extension URL
13+
*/
14+
export const isInternalOrigin = (
15+
sender: IMessageSender | undefined,
16+
source: string,
17+
): boolean => {
18+
const extensionOrigin = chrome.runtime.getURL('');
19+
const senderUrl = sender?.url;
20+
const isInternal = senderUrl?.startsWith(extensionOrigin) ?? false;
21+
22+
if (!isInternal) {
23+
logger.error(
24+
new RainbowError(`${source}: message received from invalid origin`, {
25+
cause: new Error(`Invalid origin: ${senderUrl ?? 'unknown'}`),
26+
}),
27+
);
28+
}
29+
30+
return isInternal;
31+
};

src/entries/background/procedures/popup/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { RPCHandler } from '@orpc/server/message-port';
33
import * as Sentry from '@sentry/react';
44

55
import { INTERNAL_BUILD, IS_TESTING } from '~/core/sentry';
6+
import { isInternalOrigin } from '~/core/utils/isInternalOrigin';
67
import { RainbowError, logger } from '~/logger';
78

89
import { POPUP_PORT_NAME } from './constants';
@@ -49,6 +50,14 @@ export function startPopupRouter() {
4950

5051
chrome.runtime.onConnect.addListener((port) => {
5152
if (port.name === POPUP_PORT_NAME) {
53+
// Defense-in-depth: validate that port connections originate from extension URLs.
54+
// Port-based connections are inherently more secure than message-based,
55+
// but explicit validation protects against future changes or compromised extensions.
56+
if (!isInternalOrigin(port.sender, 'oRPC:startPopupRouter')) {
57+
port.disconnect();
58+
return;
59+
}
60+
5261
// Register port for disconnect tracking (expiry and immediate lock)
5362
registerPopupPort(port);
5463

0 commit comments

Comments
 (0)